From 6d70ca0c3226d9176c385a39cfc645fff0adbecb Mon Sep 17 00:00:00 2001 From: alban Date: Wed, 11 Nov 2020 17:31:08 +0100 Subject: [PATCH] [init] --- .gitignore | 3 + README.md | 70 + _run.sh | 11 + exports/toNull.py | 46 + exports/toRedis.py | 63 + exports/toUDP.py | 84 + exports/toWS.py | 115 ++ exports/tonano.py | 158 ++ exports/websocket_server.py | 388 ++++ filters/anaglyph.py | 200 ++ filters/colorcycle.py | 108 ++ filters/kaleidoscope.py | 174 ++ filters/redilysis.py | 300 +++ filters/redilysis_colors.py | 186 ++ generators/159.gml | 2407 ++++++++++++++++++++++++ generators/160.gml | 2791 ++++++++++++++++++++++++++++ generators/OSC3.py | 2873 +++++++++++++++++++++++++++++ generators/blank.py | 53 + generators/book2.ild | Bin 0 -> 108656 bytes generators/brmlab1.svg | 54 + generators/brmlab2.svg | 53 + generators/dummy.py | 85 + generators/example.py | 182 ++ generators/fromGML.py | 406 ++++ generators/fromOSC.py | 126 ++ generators/fromRedis.py | 72 + generators/fromUDP.py | 84 + generators/fromilda.py | 319 ++++ generators/osc2redis.py | 131 ++ generators/redilysis_lines.py | 174 ++ generators/redilysis_particles.py | 288 +++ generators/text.py | 94 + generators/trckr.py | 175 ++ generators/tunnel.py | 194 ++ generators/turtle.py | 657 +++++++ generators/turtle1.py | 19 + runner.py | 126 ++ runner_lib.py | 379 ++++ runner_midi.py | 87 + 39 files changed, 13735 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 _run.sh create mode 100755 exports/toNull.py create mode 100644 exports/toRedis.py create mode 100644 exports/toUDP.py create mode 100644 exports/toWS.py create mode 100644 exports/tonano.py create mode 100644 exports/websocket_server.py create mode 100755 filters/anaglyph.py create mode 100755 filters/colorcycle.py create mode 100755 filters/kaleidoscope.py create mode 100755 filters/redilysis.py create mode 100644 filters/redilysis_colors.py create mode 100755 generators/159.gml create mode 100755 generators/160.gml create mode 100644 generators/OSC3.py create mode 100755 generators/blank.py create mode 100755 generators/book2.ild create mode 100755 generators/brmlab1.svg create mode 100755 generators/brmlab2.svg create mode 100755 generators/dummy.py create mode 100755 generators/example.py create mode 100644 generators/fromGML.py create mode 100644 generators/fromOSC.py create mode 100755 generators/fromRedis.py create mode 100644 generators/fromUDP.py create mode 100644 generators/fromilda.py create mode 100644 generators/osc2redis.py create mode 100755 generators/redilysis_lines.py create mode 100755 generators/redilysis_particles.py create mode 100644 generators/text.py create mode 100644 generators/trckr.py create mode 100755 generators/tunnel.py create mode 100644 generators/turtle.py create mode 100644 generators/turtle1.py create mode 100755 runner.py create mode 100644 runner_lib.py create mode 100755 runner_midi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb6572b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.*swp* +*__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b69e4a5 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Chaining Lasers In Submission Tools for LJ + +Alright everybody, ready for some fun? Here comes The Piping And Plumbing Your Way To The Top Show! + +You're going to push so many points to this laser it will hog and cry... + +BOOM | WIIIIIZ :: PHHHHHRACKRACKRACK ~~ WOOP ~~###~~ WIIT + + +## The basic loop +``` +python3 generators/dummy.py -f 2 | python3 filters/kaleidoscope.py | python3 exports/toRedis.py -v + ------------------------------ --------------------- ------------------- + \/ \/ \/ + Generator Filter Export +``` + +### 1. The Generator + +Use it to produce some points in any manner, orderly or total chaos. + +Don't be that boring Sinusoids bugger! Flash Maps of Dooms, Disbitnic sprites, Dismorphic HexaFonts all over the walls! + +### 2. The Filter(s) + +These babies will modify data on the wire by passing around the points and modifying them in sequence. + +Want your Double Heavy Laser Cannons to Bounce Together Like They Been Drinking Jagerbombs For Two Hours? That's the place. + +### 3. The Exporter + +Now, this IS the most boring part. Send your points to whatever output system. Yawn. Are we there yet? + +## Hacking around + +Say what!? Why, this is exactly the place for that! + +Take a seat and copy paste the "dummy.py" files, they present the basic structure you need to play around. + +Just be cautious to use the `debug` method if you're the kind of miss that debugs by outputing data structures (who does not, yah know, sometimes?). Or you'll break the chain. + +### Generators + +They must send list of points to standard out. Don't forget the "flush" argument, or the piping will be breaking, ain't no Mario lazering. + +* dummy.py : sends always the same list of points. The Monomaniac. +* @todo : read texts from redis and others + +### Filters + +These do listen and read on STDIN and do the same print'n'flush on STDOUT. + +* kaleidoscope.py : mirrors the points based on a pivot +* @todo : fourier analysis and other realtime reaction + +### Export + +Read from STDIN and send to redis mostly + +* toRedis.py : provide a key, server IP, etc. + +### Common parameters + +Every command can be called with a `-h/--help` flag to get some help + +Every command has a `-v/--verbose` flag to send debug info to STDERR. + +Generators have a `-f/--fps` param for FPS, to be fast but not so furious on your machine + +Filters and Exports are their own beasts diff --git a/_run.sh b/_run.sh new file mode 100755 index 0000000..96014a0 --- /dev/null +++ b/_run.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +killexit(){ + pkill -9 -s $$ + } + +trap killexit SIGTERM SIGINT SIGKILL SIGSTOP + +bash -c "$@" + +killbill(){ pkill -9 -s $(awk '{print $6}' /proc/$1/stat) } diff --git a/exports/toNull.py b/exports/toNull.py new file mode 100755 index 0000000..08b4dfc --- /dev/null +++ b/exports/toNull.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +The exporter that drops all traffic ! +v0.1.0 + +A basic exporter + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import os +import argparse +import redis +import time + +argsparser = argparse.ArgumentParser(description="Null exporter LJ") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +args = argsparser.parse_args() + +verbose=args.verbose + +name = "exports::toNull" +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + debug(name,"dumping: "+line) +except EOFError: + debug("break")# no more information + diff --git a/exports/toRedis.py b/exports/toRedis.py new file mode 100644 index 0000000..36381b8 --- /dev/null +++ b/exports/toRedis.py @@ -0,0 +1,63 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redis exporter +v0.1.0 + +A basic exporter + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import os +import argparse +import redis +import time + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +args = argsparser.parse_args() + +ip = args.ip +port = args.port +key = args.key +verbose=args.verbose + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +r=redis.StrictRedis(host=ip, port=port, db=0) + +try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + continue + line = line.rstrip('\n') + line=line[1:-1] + line = line.replace("[",'(') + line = line.replace("]",')') + line = "[{}]".format(line) + if line == "[]": + line="[(400.0,400.0,0),(400.0,400.0,0),(400.0,400.0,0),(400.0,400.0,0)]" + continue + if r.set(key,line)==True: + debug("exports::redis set("+str(key)+") to "+line) +except EOFError: + debug("break")# no more information + diff --git a/exports/toUDP.py b/exports/toUDP.py new file mode 100644 index 0000000..64787db --- /dev/null +++ b/exports/toUDP.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +toUDP +v0.1.0 + +A basic exporter + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import os +import argparse +import time +import socket +import ast + +argsparser = argparse.ArgumentParser(description="toUDP v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="UDP port to bind to (9000 by default)",default="9003",type=str) +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) +verbose = args.verbose + + +name = "exports::toUDP" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def ClientStart(ip, port): + global sockclient + + sockclient = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + +def ClientSend(msgFromClient): + + bytesToSend = str.encode(str(msgFromClient)) + serverAddressPort = (ip, port) + bufferSize = 1024 + + # Send to server using created UDP socket + sockclient.sendto(bytesToSend, serverAddressPort) + + ''' + # If reply : + msgFromServer = sockclient.recvfrom(bufferSize) + + msg = "Message from Server {}".format(msgFromServer[0]) + print(msg) + ''' + +try: + + ClientStart(ip, port) + while True: + + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + #pointsList = ast.literal_eval(line) + debug(name,": "+line) + ClientSend(line) + + +except EOFError: + debug("break")# no more information + diff --git a/exports/toWS.py b/exports/toWS.py new file mode 100644 index 0000000..1af33ed --- /dev/null +++ b/exports/toWS.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +toWS +exporter to LJ via WebSocket +v0.1b + +''' +from __future__ import print_function +import websocket +import time +import argparse +import traceback +import sys + +try: + import thread +except ImportError: + import _thread as thread + + +print("") +print("toWS v0.1b") +print ("Arguments parsing if needed...") +argsparser = argparse.ArgumentParser(description="toWS v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-s","--server",help="WS server IP (127.0.0.1 by default)", type=str) +argsparser.add_argument("-p","--port",help="WS port to bind to (9001 by default)", type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +args = argsparser.parse_args() + +key = args.key +verbose=args.verbose + +name = "exports::toWS" + + + +# Server name +if args.server: + serverIP = args.server +else: + serverIP = "127.0.0.1" + +# ORCA destination device +if args.port: + wsPORT = args.port +else: + wsPORT = 9001 + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + + +def on_message(ws, message): + debug(message) + +def on_error(ws, error): + debug(error) + +def on_close(ws): + debug("### closed ###") + +def on_open(ws): + + def run(*args): + + try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + line=line[1:-1] + line = line.replace("[",'(') + line = line.replace("]",')') + #debug(line) + line = "[{}]".format(line) + ws.send(str(key)+' "'+line+'"') + debug("exports::ws "+str(key)+" "+line) + + + except EOFError: + debug("break")# no more information + + finally: + ws.close() + print("sendWS terminating...") + + + thread.start_new_thread(run, ()) + + +if __name__ == "__main__": + websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + ws.run_forever() + + + + + + diff --git a/exports/tonano.py b/exports/tonano.py new file mode 100644 index 0000000..3d9e0f5 --- /dev/null +++ b/exports/tonano.py @@ -0,0 +1,158 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +tonano +exporter to LJ nano +v0.1b + +''' +from __future__ import print_function +import websocket +import time +import argparse +import traceback +import sys +import random +from websocket_server import WebsocketServer +from socket import * + +try: + import thread +except ImportError: + import _thread as thread + +name = "exports::tonano" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="tonano v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-s","--server",help="WS server IP (127.0.0.1 by default)", type=str) +argsparser.add_argument("-p","--port",help="WS port to bind to (9001 by default)", type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +args = argsparser.parse_args() + +key = args.key + +if args.verbose: + verbose = True +else: + verbose = False + +if args.server: + serverIP = args.server +else: + serverIP = "127.0.0.1" + +if args.port: + wsPORT = args.port +else: + wsPORT = 9001 + +debug("") +debug("tonano v0.1b") + +points0 = "[(150.0, 230.0, 65280), (170.0, 170.0, 65280), (230.0, 170.0, 65280), (210.0, 230.0, 65280), (150.0, 230.0, 65280)]" +points1 = "[(180.0, 230.0, 65280), (200.0, 170.0, 65280), (180.0, 230.0, 65280)]" +points2 = "[(170.0, 190.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (230.0, 200.0, 65280), (170.0, 230.0, 65280), (230.0, 230.0, 65280)]" +points3 = "[(170.0, 170.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (200.0, 200.0, 65280), (230.0, 210.0, 65280), (200.0, 230.0, 65280), (170.0, 230.0, 65280)]" +points = [points0, points1, points2, points3] + +LaserNumber = 1 +SceneNumber = 0 +Laser = 0 + +def sendbroadcast(): + + debug("Sending broadcast") + cs = socket(AF_INET, SOCK_DGRAM) + cs.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + cs.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + cs.sendto("LJ tonano 0.1".encode(), ("255.255.255.255", 54545)) + + +# +# CLI websocket client -> WS server (nanoLJ) -> webpage +# + +def on_message(ws, message): + if random.randint(0,100)>95: + sendbroadcast() + #debug("CLI WS client received and dropped "+message) + +def on_error(ws, error): + debug("CLI WS client got error :"+str(error)) + +def on_close(ws): + debug("### CLI WS client WS closed ###") + +def on_open(ws): + + def run(*args): + + try: + while True: + line = sys.stdin.readline() + if line == "": + time.sleep(0.005) + + #debug("CLI string", line) + line = line.rstrip('\n') + line=line[1:-1] + line = line.replace("[",'(') + line = line.replace("]",')') + #debug(line) + line = "[{}]".format(line) + debug("CLI proccess sending : /simul" +" "+ line) + #sendWSall("/simul" +" "+ str(points[laserid].decode('ascii'))) + ws.send("/simul "+line) + #debug("exports::tosimuCLIent "+str(key)+" "+line) + + except EOFError: + debug("tonano break")# no more information + + finally: + ws.close() + debug("tonano WS terminating...") + + + thread.start_new_thread(run, ()) + +def handle_timeout(self): + self.timed_out = True + + +# +# Launch WS CLI client +# + +if __name__ == "__main__": + + try: + + # CLI Websocket client + debug("Launching tosimu CLI websocket client...") + #websocket.enableTrace(True) + websocket.enableTrace(False) + print("ws://"+str(serverIP)+":"+str(wsPORT)) + ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + ws.run_forever() + + except Exception: + debug("tonano Exception") + traceback.print_exc() + + + + + diff --git a/exports/websocket_server.py b/exports/websocket_server.py new file mode 100644 index 0000000..50844a0 --- /dev/null +++ b/exports/websocket_server.py @@ -0,0 +1,388 @@ +# Author: Johan Hanssen Seferidis +# License: MIT + +''' +Custom version +with clients_list() +For 2 clients : +[{'id': 1, 'handler': , 'address': ('127.0.0.1', 62718)}, {'id': 2, 'handler': , 'address': ('127.0.0.1', 62720)}] + + +def client_list(): + + clients = wserver.clients() + for client in clients: + print(client['id']) +''' + +import sys +import struct +from base64 import b64encode +from hashlib import sha1 +import logging +from socket import error as SocketError +import errno + +if sys.version_info[0] < 3: + from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler +else: + from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler + +logger = logging.getLogger(__name__) +logging.basicConfig() + +''' ++-+-+-+-+-------+-+-------------+-------------------------------+ + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Extended payload length | +|I|S|S|S| (4) |A| (7) | (16/64) | +|N|V|V|V| |S| | (if payload len==126/127) | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +| Extended payload length continued, if payload len == 127 | ++ - - - - - - - - - - - - - - - +-------------------------------+ +| Payload Data continued ... | ++---------------------------------------------------------------+ +''' + +FIN = 0x80 +OPCODE = 0x0f +MASKED = 0x80 +PAYLOAD_LEN = 0x7f +PAYLOAD_LEN_EXT16 = 0x7e +PAYLOAD_LEN_EXT64 = 0x7f + +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE_CONN = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xA + + +# -------------------------------- API --------------------------------- + +class API(): + + def run_forever(self): + try: + logger.info("Listening on port %d for clients.." % self.port) + self.serve_forever() + except KeyboardInterrupt: + self.server_close() + logger.info("Server terminated.") + except Exception as e: + logger.error(str(e), exc_info=True) + exit(1) + + def new_client(self, client, server): + pass + + def client_left(self, client, server): + pass + + def message_received(self, client, server, message): + pass + + def set_fn_new_client(self, fn): + self.new_client = fn + + def set_fn_client_left(self, fn): + self.client_left = fn + + def set_fn_message_received(self, fn): + self.message_received = fn + + def send_message(self, client, msg): + self._unicast_(client, msg) + + def send_message_to_all(self, msg): + self._multicast_(msg) + + def clients_list(self): + return self.clients + + +# ------------------------- Implementation ----------------------------- + +class WebsocketServer(ThreadingMixIn, TCPServer, API): + """ + A websocket server waiting for clients to connect. + + Args: + port(int): Port to bind to + host(str): Hostname or IP to listen for connections. By default 127.0.0.1 + is being used. To accept connections from any client, you should use + 0.0.0.0. + loglevel: Logging level from logging module to use for logging. By default + warnings and errors are being logged. + + Properties: + clients(list): A list of connected clients. A client is a dictionary + like below. + { + 'id' : id, + 'handler' : handler, + 'address' : (addr, port) + } + """ + + allow_reuse_address = True + daemon_threads = True # comment to keep threads alive until finished + + clients = [] + id_counter = 0 + + def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): + logger.setLevel(loglevel) + TCPServer.__init__(self, (host, port), WebSocketHandler) + self.port = self.socket.getsockname()[1] + + def _message_received_(self, handler, msg): + self.message_received(self.handler_to_client(handler), self, msg) + + def _ping_received_(self, handler, msg): + handler.send_pong(msg) + + def _pong_received_(self, handler, msg): + pass + + def _new_client_(self, handler): + self.id_counter += 1 + client = { + 'id': self.id_counter, + 'handler': handler, + 'address': handler.client_address + } + self.clients.append(client) + self.new_client(client, self) + + def _client_left_(self, handler): + client = self.handler_to_client(handler) + self.client_left(client, self) + if client in self.clients: + self.clients.remove(client) + + def _unicast_(self, to_client, msg): + to_client['handler'].send_message(msg) + + def _multicast_(self, msg): + for client in self.clients: + self._unicast_(client, msg) + + def handler_to_client(self, handler): + for client in self.clients: + if client['handler'] == handler: + return client + + +class WebSocketHandler(StreamRequestHandler): + + def __init__(self, socket, addr, server): + self.server = server + StreamRequestHandler.__init__(self, socket, addr, server) + + def setup(self): + StreamRequestHandler.setup(self) + self.keep_alive = True + self.handshake_done = False + self.valid_client = False + + def handle(self): + while self.keep_alive: + if not self.handshake_done: + self.handshake() + elif self.valid_client: + self.read_next_message() + + def read_bytes(self, num): + # python3 gives ordinal of byte directly + bytes = self.rfile.read(num) + if sys.version_info[0] < 3: + return map(ord, bytes) + else: + return bytes + + def read_next_message(self): + try: + b1, b2 = self.read_bytes(2) + except SocketError as e: # to be replaced with ConnectionResetError for py3 + if e.errno == errno.ECONNRESET: + logger.info("Client closed connection.") + print("Error: {}".format(e)) + self.keep_alive = 0 + return + b1, b2 = 0, 0 + except ValueError as e: + b1, b2 = 0, 0 + + fin = b1 & FIN + opcode = b1 & OPCODE + masked = b2 & MASKED + payload_length = b2 & PAYLOAD_LEN + + if opcode == OPCODE_CLOSE_CONN: + logger.info("Client asked to close connection.") + self.keep_alive = 0 + return + if not masked: + logger.warn("Client must always be masked.") + self.keep_alive = 0 + return + if opcode == OPCODE_CONTINUATION: + logger.warn("Continuation frames are not supported.") + return + elif opcode == OPCODE_BINARY: + logger.warn("Binary frames are not supported.") + return + elif opcode == OPCODE_TEXT: + opcode_handler = self.server._message_received_ + elif opcode == OPCODE_PING: + opcode_handler = self.server._ping_received_ + elif opcode == OPCODE_PONG: + opcode_handler = self.server._pong_received_ + else: + logger.warn("Unknown opcode %#x." % opcode) + self.keep_alive = 0 + return + + if payload_length == 126: + payload_length = struct.unpack(">H", self.rfile.read(2))[0] + elif payload_length == 127: + payload_length = struct.unpack(">Q", self.rfile.read(8))[0] + + masks = self.read_bytes(4) + message_bytes = bytearray() + for message_byte in self.read_bytes(payload_length): + message_byte ^= masks[len(message_bytes) % 4] + message_bytes.append(message_byte) + opcode_handler(self, message_bytes.decode('utf8')) + + def send_message(self, message): + self.send_text(message) + + def send_pong(self, message): + self.send_text(message, OPCODE_PONG) + + def send_text(self, message, opcode=OPCODE_TEXT): + """ + Important: Fragmented(=continuation) messages are not supported since + their usage cases are limited - when we don't know the payload length. + """ + + # Validate message + if isinstance(message, bytes): + message = try_decode_UTF8(message) # this is slower but ensures we have UTF-8 + if not message: + logger.warning("Can\'t send message, message is not valid UTF-8") + return False + elif sys.version_info < (3,0) and (isinstance(message, str) or isinstance(message, unicode)): + pass + elif isinstance(message, str): + pass + else: + logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) + return False + + header = bytearray() + payload = encode_to_UTF8(message) + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < 18446744073709551616: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big. Consider breaking it into chunks.") + return + + self.request.send(header + payload) + + def read_http_headers(self): + headers = {} + # first line should be HTTP GET + http_get = self.rfile.readline().decode().strip() + assert http_get.upper().startswith('GET') + # remaining should be headers + while True: + header = self.rfile.readline().decode().strip() + if not header: + break + head, value = header.split(':', 1) + headers[head.lower().strip()] = value.strip() + return headers + + def handshake(self): + headers = self.read_http_headers() + + try: + assert headers['upgrade'].lower() == 'websocket' + except AssertionError: + self.keep_alive = False + return + + try: + key = headers['sec-websocket-key'] + except KeyError: + logger.warning("Client tried to connect but was missing a key") + self.keep_alive = False + return + + response = self.make_handshake_response(key) + self.handshake_done = self.request.send(response.encode()) + self.valid_client = True + self.server._new_client_(self) + + @classmethod + def make_handshake_response(cls, key): + return \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: %s\r\n' \ + '\r\n' % cls.calculate_response_key(key) + + @classmethod + def calculate_response_key(cls, key): + GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + hash = sha1(key.encode() + GUID.encode()) + response_key = b64encode(hash.digest()).strip() + return response_key.decode('ASCII') + + def finish(self): + self.server._client_left_(self) + + +def encode_to_UTF8(data): + try: + return data.encode('UTF-8') + except UnicodeEncodeError as e: + logger.error("Could not encode data to UTF-8 -- %s" % e) + return False + except Exception as e: + raise(e) + return False + + +def try_decode_UTF8(data): + try: + return data.decode('utf-8') + except UnicodeDecodeError: + return False + except Exception as e: + raise(e) diff --git a/filters/anaglyph.py b/filters/anaglyph.py new file mode 100755 index 0000000..12d2203 --- /dev/null +++ b/filters/anaglyph.py @@ -0,0 +1,200 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +anaglyph +v0.1.0 + +Attempts to create a valid 3D-glasses structure + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import math +import os +import random +import sys +import time +name = "filters::cycle" + +maxDist = 300 + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-m","--min",help="Minimal displacement (default:2) ",default=1,type=int) +argsparser.add_argument("-M","--max",help="Maximal displacement (default:20) ",default=5,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") + +args = argsparser.parse_args() +fps = args.fps +minVal = args.min +maxVal = args.max +centerX = args.centerX +centerY = args.centerY +verbose = args.verbose + +optimal_looptime = 1 / fps +name = "filters::anaglyph" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def isValidColor( color, intensityColThreshold ): + if color[0] + color[1] + color[2] > intensityColThreshold: + return True + return False + +# These are paper colors +red = (41,24,24) +white = (95,95,95) +blue = (0,41,64) + +red = (127,0,0) +blue = (0,128,128) +white = (128,128,128) +def anaglyph( pl ): + + debug(name,'--------------- new loop ------------------') + # We will send one list after the other to optimize color change + blueList = list() + redList = list() + whiteList = list() + out = [] + out1 = [] + out2 = [] + out3 = [] + + # The anaglyphic effect will be optained by : + # * having close objects appear as white + # * having distant objects appear as blue + red + # * having in between objects appear as distanceDecreased(white) + blue + red + for i, point in enumerate(pl): + ref_x = point[0]-centerX + ref_y = point[1]-centerY + ref_color = point[2] + angle = math.atan2( ref_x , ref_y ) + dist = ref_y / math.cos(angle) + white_rvb = (0,0,0) + blue_rvb = (0,0,0) + red_rvb = (0,0,0) + + # Calculate the point's spread factor (0.0 to 1.0) + # The spread is high if the point is close to center + """ + dist = 0 : spread = 1.0 + dist = maxDist spread = 0.0 + """ + if dist == 0: + spread = 1.0 + else : + spread =( maxDist - dist ) / maxDist + if spread < 0.0: + spread = 0.0 + + #debug(name,"dist:{} spread:{}".format(dist,spread)) + + # White color is high if spread is low, i.e. point away from center + """ + spread = 1.0 : white_c = 0.0 + spread = 0.0 : whice_c = 1.0 + """ + if point[2] == 0: + white_color = 0 + else: + white_factor = 1.0 - math.pow(spread,0.5) + white_rvb = tuple(map( lambda a: int(white_factor* a), white)) + white_color = rgb2int( white_rvb) + #debug(name,"spread:{}\t white_rvb:{}\t white_color:{}".format(spread, white_rvb, white_color)) + + # Blue and Red colors are high if spread is high, i.e. close to center + """ + spread = 1.0 : red_c = 1.0 + spread = 0.0 : red_c = 0.0 + """ + color_factor = math.pow(spread,1) + if point[2] == 0: + blue_color = 0 + red_color = 0 + else: + blue_rvb = tuple(map( lambda a: int(color_factor * a), blue)) + blue_color = rgb2int( blue_rvb) + red_rvb = tuple(map( lambda a: int(color_factor * a), red)) + red_color = rgb2int( red_rvb) + + #debug(name,"color_factor:{}\t\t blue_color:{}\t\t red_color:{}".format(color_factor,blue_color,red_color)) + + # Blue-to-Red spatial spread is high when spread is high, i.e. point close to center + """ + spread = 1.0 : spatial_spread = maxVal + spread = 0.0 : spatial_spread = minVal + """ + spatial_spread = minVal + spread * (maxVal - minVal) + #debug(name,"spatial_spread:{}".format(spatial_spread)) + red_x = int(point[0] + spatial_spread) + blue_x = int(point[0] - spatial_spread ) + red_y = int(point[1] ) + blue_y = int(point[1]) + + white_point = [point[0], point[1], white_color] + blue_point = [blue_x,blue_y,blue_color] + red_point = [red_x,red_y,red_color] + + #debug(name,"white[x,y,c]:{}".format(white_point)) + #debug(name,"blue[x,y,c]:{}".format(blue_point)) + #debug(name,"red[x,y,c]:{}".format(red_point)) + # Do not append "black lines" i.e. a color where each composent is below X +# if isValidColor(white_rvb, 150): +# out1.append(white_point) +# if isValidColor(blue_rvb, 50): +# out2.append(blue_point) +# if isValidColor(red_rvb, 30): +# out3.append(red_point) + out1.append(white_point) + out2.append(blue_point) + out3.append(red_point) + + #debug(name,"source pl:{}".format(pl)) + debug(name,"whiteList:{}".format(out1)) + debug(name,"blueList:{}".format(out2)) + debug(name,"redList:{}".format(out3)) + return out1 + out3 + out2 + #return out1 + out2 + out3 + + + +try: + while True: + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + result = anaglyph( pointsList ) + print( result, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/filters/colorcycle.py b/filters/colorcycle.py new file mode 100755 index 0000000..e293336 --- /dev/null +++ b/filters/colorcycle.py @@ -0,0 +1,108 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +colorcycle +v0.1.0 + +A simple effect : cycle colors + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import sys +import ast +import os +import argparse +import random +import time +name = "filters::cycle" + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=300,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=300,type=int) +argsparser.add_argument("-m","--min",help="Lowest value in the range 0-255",default=10,type=int) +argsparser.add_argument("-M","--max",help="Highest value in the range 0-255",default=255,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") + +args = argsparser.parse_args() +fps = args.fps +minVal = args.min +maxVal = args.max +centerX = args.centerX +centerY = args.centerY +verbose = args.verbose + +optimal_looptime = 1 / fps + +UP = 5 +DOWN = -5 +currentColor = [0,0,0] +composant = 0 +currentDirection = UP + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def cycleColor( pl ): + + global composant + global currentDirection + # debug(name,"pl:{}".format(pl)) + value = currentColor[composant] + if currentDirection == UP: + target = maxVal + else: + target = minVal + value += currentDirection + currentColor[composant] = value + + debug(name,"currentColor:{}".format(currentColor)) + for i in range( 0, len(pl)): + if pl[i][2] != 0: + pl[i][2] = rgb2int( currentColor) + + # change the composant if target reached + if value <= target and currentDirection == DOWN or value >= target and currentDirection == UP : + composant = random.randint( 0,2) + value = currentColor[composant] + if value == 0 : + currentDirection = UP + else: + currentDirection = DOWN + #debug( "pl:{}".format(pl)) + return pl + + +try: + while True: + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + result = cycleColor( pointsList ) + print( result, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/filters/kaleidoscope.py b/filters/kaleidoscope.py new file mode 100755 index 0000000..5ca4396 --- /dev/null +++ b/filters/kaleidoscope.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +kaleidoscop +v0.1.0 + +A simple effect : mirror a quadrant of the input + +LICENCE : CC + +by Sam Neurohack + + +''' +from __future__ import print_function +import sys +import ast +import os +import argparse +ljpath = r'%s' % os.getcwd().replace('\\','/') +sys.path.append(ljpath +'/../libs/') +sys.path.append(ljpath +'/libs/') + +import time +name = "filters::kaleidoscope" + +argsparser = argparse.ArgumentParser(description="Redis exporter LJ") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") + +args = argsparser.parse_args() +fps = args.fps +centerX = args.centerX +centerY = args.centerY +verbose = args.verbose + +optimal_looptime = 1 / fps + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +def kaleidoscope( pl ): + + # Stage 1: Crop points in single quadrant + quad1 = [] + # Iterate trough the segments + for i in range( 0, len(pl) ): + + #debug(name+" point #", i) + currentpoint = cp = pl[i] + cx,cy,cc = [cp[0],cp[1],cp[2]] + + # Exception: escape early if last point + if i == len(pl) - 1: + if cx >= centerX and cy >= centerY : + quad1.append( currentpoint ) + break + + # Search for the couple of points + nextpoint = pl[i+1] + nx,ny,nc = [nextpoint[0],nextpoint[1],nextpoint[2]] + rect=[[cx,cy],[cx,ny],[nx,ny],[nx,cy]] + + right = wrong = 0 + #debug(name+" rect: ", rect,"curr",currentpoint,"next",nextpoint ) + + # Enumerate the points in rectangle to see + # how many right / wrong there are + # either to add or skip early + for iterator, p in enumerate(rect): + if p[0] >= centerX and p[1] >= centerY: + right += 1 + else: + #if p[0] <= centerX and p[1] <= centerY: + wrong += 1 + # If all rectangle points are in the right quadrant, Add and Skip + if right == 4: + quad1.append(pl[i]) + #debug(name+" found valid point", pl[i]) + continue + # If all rectangle points in wrong quadrant, Skip + if wrong == 4: + #debug(name+" found bad point", pl[i]) + continue + + # Find the (x,y) intersections + # + #debug(name+" Looking for crossing point between ("+str(cx)+","+str(cy)+") and ("+str(nx)+","+str(ny)+")") + delta=[ nx - cx, ny - cy ] + #debug(name+" delta:",delta) + crossX = None + crossY = None + absnewX = 0 + absnewY = 0 + # If one point has negative x, search y axis crossing + if cx < centerX or nx < centerX: + if delta[0] == 0 : + delta[0] = 0.0000001 + v=[ delta[0]/abs(delta[0]), delta[1]/abs(delta[0]) ] + absnewX = abs( centerX - cx ) + #print("on y axis, v=",str(v)," and absnewX=",str(absnewX)) + crossX = [( absnewX*v[0] + cx ),( absnewX*v[1]+cy ), nc] + # If one point has negative y, search x axis crossing + if cy < centerY or ny < centerY: + if delta[1] == 0 : + delta[1] = 0.0000001 + v=[ delta[0]/abs(delta[1]), delta[1]/abs(delta[1])] + absnewY = abs( centerY - cy ) + #print("on x axis, v=",str(v)," and absnewY=",str(absnewY)) + crossY = [( absnewY*v[0] + cy ),( absnewY*v[1]+cy ), nc] + # Inject in order + # If current point is the quadrant, add it + if cx >= centerX and cy >= centerY : + quad1.append( currentpoint ) + # If absnewX smaller, it is closest to currentPoint + if absnewX < absnewY: + if None != crossX : quad1.append( crossX ) + if None != crossY : quad1.append( crossY ) + else : + if None != crossY : quad1.append( crossY ) + if None != crossX : quad1.append( crossX ) + # Add a black point at the end + #lastQuad1Point = quad1[-1] + #quad1.append( [lastQuad1Point[0],lastQuad1Point[1],0] ) + + ## Stage 2 : Mirror points + # + quad2 = [] + # quad2 = vertical symetric of quad1 + for iterator in range( len(quad1) -1 , -1, -1): + point = quad1[iterator] + quad2.append([ point[0], 2*centerY - point[1], point[2] ]) + # quad3 is the merge of 1 and 2 + quad3 = quad1 + quad2 + # quad4 is the horizontal symetric of quad3 + quad4 = [] + for iterator in range( len(quad3) -1, -1, -1): + point = quad3[iterator] + quad4.append([ 2*centerX - point[0], point[1], point[2] ]) + + #debug(name+" quad1:",quad1) + #debug(name+" quad2:", quad2 ) + #debug(name+" quad3:", quad3 ) + #debug(name+" quad4:", quad4 ) + return quad3+quad4 + +try: + while True: + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + result = kaleidoscope( pointsList ) + print( result, flush=True ) + + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/filters/redilysis.py b/filters/redilysis.py new file mode 100755 index 0000000..00cd61a --- /dev/null +++ b/filters/redilysis.py @@ -0,0 +1,300 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis +v0.1.0 + +A complex effect that depends on redis keys for audio analysis + +see https://git.interhacker.space/teamlase/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "filters::redilysis" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +# The list of available modes => redis keys each requires to run +oModeList = { + "rms_noise": ["rms"], + "rms_size": ["rms"], + "bpm_size": ["bpm"], + "bpm_detect_size": ["bpm","bpm_delay","bpm_sample_interval","beats"] + } +CHAOS = 1 +REDISLATENCY = 30 +REDIS_FREQ = 300 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-s","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# General args +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +# Modes And Common Modes Parameters +argsparser.add_argument("-l","--redisLatency",help="Latency in ms to substract. Default:{}".format(REDISLATENCY),default=REDISLATENCY,type=float) +argsparser.add_argument("-m","--modelist",required=True,help="Comma separated list of modes to use from: {}".format("i, ".join(oModeList.keys())),type=str) +argsparser.add_argument("--chaos",help="How much disorder to bring. High value = More chaos. Default {}".format(CHAOS), default=CHAOS, type=str) + +args = argsparser.parse_args() +ip = args.ip +port = args.port +redisFreq = args.redis_freq / 1000 +verbose = args.verbose +fps = args.fps +centerX = args.centerX +centerY = args.centerY +redisLatency = args.redisLatency +chaos = float(args.chaos) +optimal_looptime = 1 / fps + +modeList = args.modelist.split(",") +redisKeys = [] +for mode in modeList: + if not mode in oModeList: + print("Mode '{}' is invalid. Exiting.".format(mode)) + sys.exit(2) + redisKeys += oModeList[mode] +redisKeys = list(set(redisKeys)) +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +# Records the last bpm +tsLastBeat = time.time() + +def gauss(x, mu, sigma): + return( math.exp(-math.pow((x-mu),2)/(2*math.pow(sigma,2))/math.sqrt(2*math.pi*math.pow(sigma,2)))) + +previousPTTL = 0 +tsNextBeatsList = [] +def bpmDetect( ): + """ + An helper to compute the next beat time in milliseconds + Returns True if the cache was updated + """ + global tsNextBeatsList + global previousPTTL + global redisLastHit + global redisLatency + + # Get the redis PTTL value for bpm + PTTL = redisData["bpm_pttl"] + + # Skip early if PTTL < 0 + if PTTL < 0 : + debug(name,"bpmDetect skip detection : PTTL expired for 'bpm' key") + return False + + # Skip early if the record hasn't been rewritten + if PTTL <= previousPTTL : + previousPTTL = PTTL + #debug(name,"bpmDetect skip detection : {} <= {}".format(PTTL, previousPTTL)) + return False + debug(name,"bpmDetect running detection : {} > {}".format(PTTL, previousPTTL)) + previousPTTL = PTTL + + # Skip early if beat list is empty + beatsList = ast.literal_eval(redisData["beats"]) + tsNextBeatsList = [] + if( len(beatsList) == 0 ): + return True + + # Read from redis + bpm = float(redisData["bpm"]) + msBpmDelay = float(redisData["bpm_delay"]) + samplingInterval = float(redisData["bpm_sample_interval"]) + + # Calculate some interpolations + lastBeatTiming = float(beatsList[len(beatsList) - 1]) + msPTTLDelta = 2 * samplingInterval - float(PTTL) + sPerBeat = 60 / bpm + lastBeatDelay = msBpmDelay - lastBeatTiming*1000 + msPTTLDelta + countBeatsPast = math.floor( (lastBeatDelay / 1000) / sPerBeat) + #debug(name,"bpmDetect lastBeatTiming:{}\tmsPTTLDelta:{}\tsPerBeat:{}".format(lastBeatTiming,msPTTLDelta,sPerBeat)) + #debug(name,"lastBeatDelay:{}\t countBeatsPast:{}".format(lastBeatDelay, countBeatsPast)) + for i in range( countBeatsPast, 1000): + beatTime = i * sPerBeat - lastBeatTiming + if beatTime < 0: + continue + if beatTime * 1000 > 2 * samplingInterval : + break + #debug(name, "bpmDetect beat add beatTime:{} redisLastHit:{}".format(beatTime, redisLastHit)) + tsNextBeatsList.append( redisLastHit + beatTime - redisLatency/1000) + debug(name, "bpmDetect new tsNextBeatsList:{}".format(tsNextBeatsList)) + + return True + +def bpm_detect_size( pl ): + bpmDetect() + + # Find the next beat in the list + tsNextBeat = 0 + + now = time.time() + msNearestBeat = None + msRelativeNextBTList = list(map( lambda a: abs(now - a) * 1000, tsNextBeatsList)) + msToBeat = min( msRelativeNextBTList) + + #debug(name,"bpm_detect_size msRelativeNextBTList:{} msToBeat:{}".format(msRelativeNextBTList,msToBeat)) + # Calculate the intensity based on bpm coming/leaving + # The curb is a gaussian + mu = 15 + intensity = gauss( msToBeat, 0 , mu) + #debug(name,"bpm_size","mu:{}\t msToBeat:{}\tintensity:{}".format(mu, msToBeat, intensity)) + if msToBeat < 20: + debug(name,"bpm_detect_size kick:{}".format(msToBeat)) + pass + for i, point in enumerate(pl): + ref_x = point[0]-centerX + ref_y = point[1]-centerY + #debug(name,"In new ref x:{} y:{}".format(point[0]-centerX,point[1]-centerY)) + angle=math.atan2( point[0] - centerX , point[1] - centerY ) + l = ref_y / math.cos(angle) + new_l = l * intensity + #debug(name,"bpm_size","angle:{} l:{} new_l:{}".format(angle,l,new_l)) + new_x = math.sin(angle) * new_l + centerX + new_y = math.cos(angle) * new_l + centerY + #debug(name,"x,y:({},{}) x',y':({},{})".format(point[0],point[1],new_x,new_y)) + pl[i][0] = new_x + pl[i][1] = new_y + #debug( name,"bpm_detect_size output:{}".format(pl)) + return( pl ); + +def bpm_size( pl ): + global tsLastBeat + bpm = float(redisData["bpm"]) + # msseconds ber beat + msPerBeat = int(60 / bpm * 1000) + # Calculate the intensity based on bpm coming/leaving + # The curb is a gaussian + mu = math.sqrt(msPerBeat) + msTimeToLastBeat = (time.time() - tsLastBeat) * 1000 + msTimeToNextBeat = (msPerBeat - msTimeToLastBeat) + intensity = gauss( msTimeToNextBeat, 0 , mu) + debug(name,"bpm_size","msPerBeat:{}\tmu:{}".format(msPerBeat, mu)) + debug(name,"bpm_size","msTimeToLastBeat:{}\tmsTimeToNextBeat:{}\tintensity:{}".format(msTimeToLastBeat, msTimeToNextBeat, intensity)) + if msTimeToNextBeat <= 0 : + tsLastBeat = time.time() + for i, point in enumerate(pl): + ref_x = point[0]-centerX + ref_y = point[1]-centerY + #debug(name,"In new ref x:{} y:{}".format(point[0]-centerX,point[1]-centerY)) + angle=math.atan2( point[0] - centerX , point[1] - centerY ) + l = ref_y / math.cos(angle) + new_l = l * intensity + #debug(name,"bpm_size","angle:{} l:{} new_l:{}".format(angle,l,new_l)) + new_x = math.sin(angle) * new_l + centerX + new_y = math.cos(angle) * new_l + centerY + #debug(name,"x,y:({},{}) x',y':({},{})".format(point[0],point[1],new_x,new_y)) + pl[i][0] = new_x + pl[i][1] = new_y + #debug( name,"bpm_noise output:{}".format(pl)) + return pl + +def rms_size( pl ): + rms = float(redisData["rms"]) + for i, point in enumerate(pl): + + ref_x = point[0]-centerX + ref_y = point[1]-centerY + debug(name,"In new ref x:{} y:{}".format(point[0]-centerX,point[1]-centerY)) + angle=math.atan2( point[0] - centerX , point[1] - centerY ) + l = ref_y / math.cos(angle) + debug(name,"angle:{} l:{}".format(angle,l)) + new_l = l + rms * chaos + new_x = math.sin(angle) * new_l + centerX + new_y = math.cos(angle) * new_l + centerY + debug(name,"x,y:({},{}) x',y':({},{})".format(point[0],point[1],new_x,new_y)) + pl[i][0] = new_x + pl[i][1] = new_y + #debug( name,"rms_noise output:{}".format(pl)) + return pl + +def rms_noise( pl ): + rms = float(redisData["rms"]) + debug(name, "pl:{}".format(pl)) + for i, point in enumerate(pl): + #debug(name,"rms_noise chaos:{} rms:{}".format(chaos, rms)) + xRandom = random.uniform(-1,1) * rms * chaos + yRandom = random.uniform(-1,1) * rms * chaos + #debug(name,"rms_noise xRandom:{} yRandom:{}".format(xRandom, yRandom)) + pl[i][0] += xRandom + pl[i][1] += yRandom + #debug( name,"rms_noise output:{}".format(pl)) + return pl + + +def refreshRedis(): + global redisLastHit + global redisData + # Skip if cache is sufficent + diff = msNow() - redisLastHit + if diff < redisFreq : + #debug(name, "refreshRedis not updating redis, {} < {}".format(diff, redisFreq)) + pass + else: + #debug(name, "refreshRedis updating redis, {} > {}".format(diff, redisFreq)) + redisLastHit = msNow() + for key in redisKeys: + redisData[key] = r.get(key).decode('ascii') + #debug(name,"refreshRedis key:{} value:{}".format(key,redisData[key])) + # Only update the TTLs + if 'bpm' in redisKeys: + redisData['bpm_pttl'] = r.pttl('bpm') + #debug(name,"refreshRedis key:bpm_ttl value:{}".format(redisData["bpm_pttl"])) + #debug(name,"redisData:{}".format(redisData)) + return True + +try: + while True: + refreshRedis() + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + for mode in modeList: + pointsList = locals()[mode](pointsList) + print( pointsList, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/filters/redilysis_colors.py b/filters/redilysis_colors.py new file mode 100644 index 0000000..230fa08 --- /dev/null +++ b/filters/redilysis_colors.py @@ -0,0 +1,186 @@ + +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis colors +v0.1.0 + +A complex effect that depends on redis keys for audio analysis + +see https://git.interhacker.space/teamlase/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "filters::redilysis_colors" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +# The list of available modes => redis keys each requires to run +oModeList = { + } + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def int2rgb(intcode): + #hexcode = hex(intcode)[2:] + hexcode = '{0:06X}'.format(intcode) + return tuple(int(hexcode[i:i+2], 16) for i in (0, 2, 4)) + #return tuple(map(ord,hexcode[1:].decode('hex'))) + + + +CHAOS = 1 +REDIS_FREQ = 100 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-s","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# Modes And Common Modes Parameters +#argsparser.add_argument("-m","--modelist",required=False,help="Comma separated list of modes to use from: {}".format("i, ".join(oModeList.keys())),type=str) +argsparser.add_argument("-c","--chaos",help="How much disorder to bring. High value = More chaos. Default {}".format(CHAOS), default=CHAOS, type=float) + +args = argsparser.parse_args() +fps = args.fps +ip = args.ip +port = args.port +redisFreq = args.redis_freq / 1000 +verbose = args.verbose +chaos = float(args.chaos) +optimal_looptime = 1 / fps + +max_width = 800 +max_height = 800 + +redisKeys = ["rms","spectrum_10","spectrum_120"] + +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + + +def refreshRedis(): + global redisData + for key in redisKeys: + try: + redisData[key] = ast.literal_eval(r.get(key).decode('ascii')) + except : + debug("Error when reading redis key '{}".format(key)) + +def gauss(x, mu, sigma): + return( math.exp(-math.pow((x-mu),2)/(2*math.pow(sigma,2))/math.sqrt(2*math.pi*math.pow(sigma,2)))) + + +spect10Correct = [ + + 6.0, + 1.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.8, + 0.6, + 0.5, + +] + +def default( pl ): + global redisData + spect = redisData["spectrum_10"] + debug(name, "spect:{}".format(spect)) + new_list = [] + + # We want to color points that are on left and right when high is strong + # i.e. the farther the distance from spectrum, the higher notes have influence + # power = 0-1 + # x = 800 spec[2]= 6.0 spec[7]=0.0 power=0.0 + # x = 0 spec[2]= 6.0 spec[7]=0.0 power=0.0 + # x = 0 spec[2]= 1.0 spec[7]=0.5 power=1.0 + + # dist 0 = 1 + # 400 - 400 : maxW/2 -x + # 399 = -1 : x - 400 + # 401 = 1 + # x = 400 spec[2]= 6.0 spec[7]=0.0 power=1.0 + # x = 400 spec[2]= 1.0 spec[7]=0.5 power=0.0 + + for i, point in enumerate(pl): + ocolor = pl[i][2] + if ocolor == 0 : + new_list.append(point) + continue + colorTuple = int2rgb(ocolor) + x = point[0] + dist = abs(x - max_width/2) + key = int(2* dist / max_width * 8) + power = spect[key] / spect10Correct[key] * chaos + color = [] + for i in colorTuple: + new_color = int(i * power) + if new_color > 255 : + new_color = 255 + if new_color < 0 : + new_color = 0 + color.append( new_color ) + color = rgb2int(tuple(color)) + + point[2] = color + new_list.append(point) + #debug(name,"x:{}\t dist:{}\t key:{}\t power:{}\t ocolor:{}\t color:{}".format(point[0], dist, key,power, ocolor, pl[i][2])) + debug( name,"rms_noise output:{}".format(new_list)) + return new_list + + +try: + while True: + refreshRedis() + start = time.time() + line = sys.stdin.readline() + if line == "": + time.sleep(0.01) + line = line.rstrip('\n') + pointsList = ast.literal_eval(line) + # Do the filter + pointsList = default(pointsList) + print( pointsList, flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/generators/159.gml b/generators/159.gml new file mode 100755 index 0000000..c121f21 --- /dev/null +++ b/generators/159.gml @@ -0,0 +1,2407 @@ + + +
+ + katsu + +
+ + + 117.000000 + -77.000000 + 0.810000 + + + 0.000000 + 0.000000 + 0.000000 + + + + + + 0.237305 + 0.406250 + 0.000000 + + + + 0.238266 + 0.403367 + 1.400000 + + + + 0.239250 + 0.399071 + 3.300000 + + + + 0.238950 + 0.393930 + 4.800000 + + + + 0.237823 + 0.387477 + 6.700000 + + + + 0.236545 + 0.380665 + 8.100000 + + + + 0.234818 + 0.372415 + 10.000000 + + + + 0.232324 + 0.363344 + 11.400000 + + + + 0.229294 + 0.353159 + 13.400000 + + + + 0.227202 + 0.345460 + 14.800000 + + + + 0.225002 + 0.338646 + 16.600000 + + + + 0.221592 + 0.334011 + 18.100000 + + + + 0.217400 + 0.330838 + 20.100000 + + + + 0.214784 + 0.331250 + 21.400000 + + + + 0.213434 + 0.333180 + 23.299999 + + + + 0.214631 + 0.333387 + 24.700001 + + + + 0.217572 + 0.333613 + 26.600000 + + + + 0.221165 + 0.337621 + 28.000000 + + + + 0.226135 + 0.344309 + 29.900000 + + + + 0.233033 + 0.350578 + 31.400000 + + + + 0.242032 + 0.357017 + 33.299999 + + + + 0.250983 + 0.362042 + 34.700001 + + + + 0.260938 + 0.366166 + 36.599998 + + + + 0.269995 + 0.367181 + 38.099998 + + + + 0.279334 + 0.366333 + 39.900002 + + + + 0.286610 + 0.364380 + 41.400002 + + + + 0.292839 + 0.361992 + 43.200001 + + + + 0.295499 + 0.360920 + 44.700001 + + + + 0.296439 + 0.361083 + 46.599998 + + + + 0.296743 + 0.363428 + 48.000000 + + + + 0.296226 + 0.366788 + 49.900002 + + + + 0.293507 + 0.368321 + 51.299999 + + + + 0.289128 + 0.370038 + 53.299999 + + + + 0.284667 + 0.375439 + 54.700001 + + + + 0.279292 + 0.383830 + 56.599998 + + + + 0.272978 + 0.392327 + 58.000000 + + + + 0.264724 + 0.402094 + 59.900002 + + + + 0.254888 + 0.412061 + 61.299999 + + + + 0.242979 + 0.421866 + 63.200001 + + + + 0.232024 + 0.425413 + 64.599998 + + + + 0.220943 + 0.425049 + 66.500000 + + + + 0.213157 + 0.421471 + 68.000000 + + + + 0.206600 + 0.415554 + 69.900002 + + + + 0.202100 + 0.408913 + 71.300003 + + + + 0.198550 + 0.402056 + 73.199997 + + + + 0.196690 + 0.396906 + 74.599998 + + + + 0.194517 + 0.386215 + 76.500000 + + + + 0.192082 + 0.372647 + 78.000000 + + + + 0.191367 + 0.363967 + 79.900002 + + + + 0.192626 + 0.350222 + 81.300003 + + + + 0.195742 + 0.331501 + 83.199997 + + + + 0.201127 + 0.314455 + 84.599998 + + + + 0.208935 + 0.298130 + 86.500000 + + + + 0.218118 + 0.289324 + 87.900002 + + + + 0.227836 + 0.284232 + 89.800003 + + + + 0.233098 + 0.282664 + 91.199997 + + + + 0.239028 + 0.283907 + 93.199997 + + + + 0.246120 + 0.286885 + 94.599998 + + + + 0.251493 + 0.290493 + 96.500000 + + + + 0.255933 + 0.294309 + 97.900002 + + + + 0.257817 + 0.296784 + 99.800003 + + + + 0.258481 + 0.300957 + 101.199997 + + + + 0.258513 + 0.306063 + 103.099998 + + + + 0.257754 + 0.308355 + 104.599998 + + + + 0.256641 + 0.308205 + 106.400002 + + + + 0.256132 + 0.304081 + 107.900002 + + + + 0.255947 + 0.298122 + 109.800003 + + + + 0.255520 + 0.294058 + 111.199997 + + + + 0.253853 + 0.287026 + 112.900002 + + + + 0.251582 + 0.276624 + 114.800003 + + + + 0.250550 + 0.264782 + 116.199997 + + + + 0.250176 + 0.252431 + 117.900002 + + + + 0.251030 + 0.243700 + 119.800003 + + + + 0.255387 + 0.226922 + 121.300003 + + + + 0.263554 + 0.202966 + 123.099998 + + + + 0.275911 + 0.180318 + 124.599998 + + + + 0.292870 + 0.157185 + 126.400002 + + + + 0.311563 + 0.140687 + 127.900002 + + + + 0.330837 + 0.127536 + 129.600006 + + + + 0.342601 + 0.121198 + 131.399994 + + + + 0.360462 + 0.115677 + 132.899994 + + + + 0.385381 + 0.110322 + 134.699997 + + + + 0.410884 + 0.108722 + 136.199997 + + + + 0.436238 + 0.108989 + 137.899994 + + + + 0.452571 + 0.111970 + 139.699997 + + + + 0.480148 + 0.124429 + 141.199997 + + + + 0.517819 + 0.145000 + 143.100006 + + + + 0.550556 + 0.168666 + 144.500000 + + + + 0.581781 + 0.196913 + 146.399994 + + + + 0.601696 + 0.223718 + 147.800003 + + + + 0.616049 + 0.249516 + 149.600006 + + + + 0.622164 + 0.264828 + 151.399994 + + + + 0.625057 + 0.287230 + 152.899994 + + + + 0.625694 + 0.316760 + 154.699997 + + + + 0.621193 + 0.341844 + 156.100006 + + + + 0.614264 + 0.364259 + 157.800003 + + + + 0.609128 + 0.376585 + 159.699997 + + + + 0.599180 + 0.392089 + 161.100006 + + + + 0.585311 + 0.411216 + 163.000000 + + + + 0.573383 + 0.425426 + 164.500000 + + + + 0.563464 + 0.436341 + 166.399994 + + + + 0.562424 + 0.438469 + 167.800003 + + + + 0.565697 + 0.436409 + 169.600006 + + + + 0.568010 + 0.434087 + 171.300003 + + + + 0.571403 + 0.428415 + 172.800003 + + + + 0.575579 + 0.419926 + 174.699997 + + + + 0.578187 + 0.411397 + 176.100006 + + + + 0.579963 + 0.403025 + 177.699997 + + + + 0.579101 + 0.397602 + 179.699997 + + + + 0.572211 + 0.388333 + 181.100006 + + + + 0.560337 + 0.376165 + 183.000000 + + + + 0.546285 + 0.367486 + 184.399994 + + + + 0.529320 + 0.361160 + 186.300003 + + + + 0.513171 + 0.360249 + 187.800003 + + + + 0.497612 + 0.361897 + 189.600006 + + + + 0.489072 + 0.362779 + 191.300003 + + + + 0.478694 + 0.363116 + 192.800003 + + + + 0.466592 + 0.365000 + 194.600006 + + + + 0.459726 + 0.373001 + 196.100006 + + + + 0.455583 + 0.383964 + 197.699997 + + + + 0.455467 + 0.389494 + 199.600006 + + + + 0.461772 + 0.393773 + 201.099991 + + + + 0.472622 + 0.397396 + 202.899994 + + + + 0.484001 + 0.397476 + 204.399994 + + + + 0.495359 + 0.395824 + 206.300003 + + + + 0.500694 + 0.396154 + 207.699997 + + + + 0.504116 + 0.401060 + 209.500000 + + + + 0.507142 + 0.407704 + 211.300003 + + + + 0.509173 + 0.409010 + 212.800003 + + + + 0.510549 + 0.408760 + 214.600006 + + + + 0.510377 + 0.413436 + 216.000000 + + + + 0.509656 + 0.423175 + 217.699997 + + + + 0.510039 + 0.437761 + 219.600006 + + + + 0.511007 + 0.454457 + 221.000000 + + + + 0.512445 + 0.465432 + 223.000000 + + + + 0.517009 + 0.484037 + 224.399994 + + + + 0.523108 + 0.506813 + 226.199997 + + + + 0.526299 + 0.519175 + 227.699997 + + + + 0.529222 + 0.532477 + 229.500000 + + + + 0.532194 + 0.546901 + 231.199997 + + + + 0.533497 + 0.553280 + 232.699997 + + + + 0.532559 + 0.554776 + 234.500000 + + + + 0.526393 + 0.551951 + 236.000000 + + + + 0.515800 + 0.545929 + 237.600006 + + + + 0.503077 + 0.537723 + 239.500000 + + + + 0.489564 + 0.528625 + 241.000000 + + + + 0.480308 + 0.523430 + 242.899994 + + + + 0.463340 + 0.516762 + 244.300003 + + + + 0.442252 + 0.509113 + 246.100006 + + + + 0.429044 + 0.505096 + 247.800003 + + + + 0.408866 + 0.501124 + 249.399994 + + + + 0.382391 + 0.497741 + 251.199997 + + + + 0.361137 + 0.499471 + 252.800003 + + + + 0.341117 + 0.505065 + 254.500000 + + + + 0.325356 + 0.513125 + 256.000000 + + + + 0.310059 + 0.523336 + 257.799988 + + + + 0.297048 + 0.532576 + 259.200012 + + + + 0.285151 + 0.541197 + 260.899994 + + + + 0.279039 + 0.544930 + 262.799988 + + + + 0.272821 + 0.546257 + 264.299988 + + + + 0.266187 + 0.546688 + 266.000000 + + + + 0.262526 + 0.545294 + 267.799988 + + + + 0.258181 + 0.538467 + 269.399994 + + + + 0.252331 + 0.527450 + 271.100006 + + + + 0.245771 + 0.516021 + 272.700012 + + + + 0.237181 + 0.502877 + 274.399994 + + + + 0.226215 + 0.489939 + 276.000000 + + + + 0.212237 + 0.476052 + 277.799988 + + + + 0.198120 + 0.465278 + 279.200012 + + + + 0.183146 + 0.455572 + 280.899994 + + + + 0.172635 + 0.449194 + 282.799988 + + + + 0.164410 + 0.444415 + 284.200012 + + + + 0.160954 + 0.442424 + 286.000000 + + + + 0.159557 + 0.441727 + 287.799988 + + + + 0.158342 + 0.441503 + 289.299988 + + + + 0.157638 + 0.441801 + 291.100006 + + + + 0.159560 + 0.443429 + 292.700012 + + + + 0.164166 + 0.445872 + 294.399994 + + + + 0.171964 + 0.447725 + 295.899994 + + + + 0.183510 + 0.449047 + 297.799988 + + + + 0.198072 + 0.448860 + 299.200012 + + + + 0.216656 + 0.446182 + 300.799988 + + + + 0.236024 + 0.437773 + 302.700012 + + + + 0.255533 + 0.426657 + 304.200012 + + + + 0.264077 + 0.421624 + 305.899994 + + + + 0.270242 + 0.416507 + 307.700012 + + + + 0.285242 + 0.401079 + 309.299988 + + + + 0.306944 + 0.377572 + 311.000000 + + + + 0.325403 + 0.355063 + 312.700012 + + + + 0.342980 + 0.330997 + 314.299988 + + + + 0.355254 + 0.310116 + 315.899994 + + + + 0.365175 + 0.288973 + 317.700012 + + + + 0.370345 + 0.271894 + 319.200012 + + + + 0.372310 + 0.256070 + 321.000000 + + + + 0.369063 + 0.246051 + 322.399994 + + + + 0.362007 + 0.238857 + 324.200012 + + + + 0.353067 + 0.235886 + 325.899994 + + + + 0.342262 + 0.235644 + 327.700012 + + + + 0.332780 + 0.238882 + 329.200012 + + + + 0.323099 + 0.245278 + 331.000000 + + + + 0.314945 + 0.254620 + 332.600006 + + + + 0.307750 + 0.266582 + 334.299988 + + + + 0.305395 + 0.277350 + 335.899994 + + + + 0.306559 + 0.288579 + 337.700012 + + + + 0.311927 + 0.298402 + 339.200012 + + + + 0.321557 + 0.307862 + 341.000000 + + + + 0.335558 + 0.313442 + 342.399994 + + + + 0.353981 + 0.316924 + 344.100006 + + + + 0.372326 + 0.317582 + 345.799988 + + + + 0.392738 + 0.316352 + 347.700012 + + + + 0.411319 + 0.313254 + 349.200012 + + + + 0.430198 + 0.308730 + 351.000000 + + + + 0.443847 + 0.304220 + 352.500000 + + + + 0.455290 + 0.300051 + 354.299988 + + + + 0.461868 + 0.299224 + 355.899994 + + + + 0.465540 + 0.300232 + 357.600006 + + + + 0.464523 + 0.301520 + 359.200012 + + + + 0.460185 + 0.303992 + 361.000000 + + + + 0.453440 + 0.309787 + 362.399994 + + + + 0.444113 + 0.319305 + 364.299988 + + + + 0.433524 + 0.332246 + 365.700012 + + + + 0.421915 + 0.346667 + 367.600006 + + + + 0.414575 + 0.353069 + 369.100006 + + + + 0.409007 + 0.357931 + 371.000000 + + + + 0.404972 + 0.370217 + 372.500000 + + + + 0.402204 + 0.385333 + 374.200012 + + + + 0.403513 + 0.388217 + 375.899994 + + + + 0.406782 + 0.383831 + 377.600006 + + + + 0.408335 + 0.377552 + 379.100006 + + + + 0.408907 + 0.369803 + 380.899994 + + + + 0.409096 + 0.362416 + 382.299988 + + + + 0.408545 + 0.355666 + 384.200012 + + + + 0.405816 + 0.354217 + 385.700012 + + + + 0.401068 + 0.355807 + 387.600006 + + + + 0.394958 + 0.358402 + 389.100006 + + + + 0.386951 + 0.361858 + 390.899994 + + + + 0.377958 + 0.365621 + 392.500000 + + + + 0.367797 + 0.369911 + 394.200012 + + + + 0.360106 + 0.373530 + 395.799988 + + + + 0.353720 + 0.376353 + 397.500000 + + + + 0.351009 + 0.375830 + 399.100006 + + + + 0.350846 + 0.372523 + 400.899963 + + + + 0.354108 + 0.366263 + 402.400024 + + + + 0.359899 + 0.357724 + 404.199982 + + + + 0.366497 + 0.349940 + 405.600037 + + + + 0.374134 + 0.342065 + 407.499969 + + + + 0.380713 + 0.336173 + 409.100006 + + + + 0.386883 + 0.331278 + 410.899994 + + + + 0.390289 + 0.329198 + 412.500000 + + + + 0.391362 + 0.328892 + 414.200012 + + + + 0.387780 + 0.330577 + 415.799988 + + + + 0.379452 + 0.335101 + 417.500000 + + + + 0.365258 + 0.345071 + 419.200012 + + + + 0.345925 + 0.359539 + 420.799988 + + + + 0.327136 + 0.373961 + 422.299988 + + + + 0.305583 + 0.391195 + 424.200012 + + + + 0.282553 + 0.411361 + 425.600006 + + + + 0.257187 + 0.434770 + 427.500000 + + + + 0.238864 + 0.453028 + 429.000000 + + + + 0.223230 + 0.469666 + 430.799988 + + + + 0.212653 + 0.481482 + 432.399994 + + + + 0.204680 + 0.490439 + 434.100006 + + + + 0.202330 + 0.491983 + 435.700012 + + + + 0.203973 + 0.488410 + 437.500000 + + + + 0.210313 + 0.480180 + 439.100006 + + + + 0.220521 + 0.468781 + 440.799988 + + + + 0.232309 + 0.458956 + 442.200012 + + + + 0.245988 + 0.449142 + 444.100006 + + + + 0.257461 + 0.440942 + 445.600006 + + + + 0.268508 + 0.433367 + 447.399994 + + + + 0.276488 + 0.429358 + 448.899994 + + + + 0.282595 + 0.427889 + 450.799988 + + + + 0.283685 + 0.430579 + 452.299988 + + + + 0.281773 + 0.436715 + 454.100006 + + + + 0.278308 + 0.445719 + 455.700012 + + + + 0.273276 + 0.458077 + 457.500000 + + + + 0.267071 + 0.472238 + 458.899994 + + + + 0.259583 + 0.488495 + 460.700012 + + + + 0.253053 + 0.501354 + 462.200012 + + + + 0.246716 + 0.512565 + 464.100006 + + + + 0.242491 + 0.518100 + 465.600006 + + + + 0.240103 + 0.519739 + 467.399994 + + + + 0.242347 + 0.515574 + 468.799988 + + + + 0.247951 + 0.507164 + 470.700012 + + + + 0.255252 + 0.496871 + 472.299988 + + + + 0.265144 + 0.483949 + 474.100006 + + + + 0.277446 + 0.470320 + 475.700012 + + + + 0.292135 + 0.455340 + 477.399994 + + + + 0.304081 + 0.444064 + 478.799988 + + + + 0.315483 + 0.434723 + 480.700012 + + + + 0.324345 + 0.430757 + 482.100006 + + + + 0.331603 + 0.429969 + 484.000000 + + + + 0.333213 + 0.432263 + 485.500000 + + + + 0.331672 + 0.437245 + 487.399994 + + + + 0.329092 + 0.444963 + 488.799988 + + + + 0.324664 + 0.456201 + 490.700012 + + + + 0.316508 + 0.470615 + 492.200012 + + + + 0.305199 + 0.488167 + 494.000000 + + + + 0.294439 + 0.503123 + 495.600006 + + + + 0.283174 + 0.517742 + 497.399994 + + + + 0.274307 + 0.528757 + 498.799988 + + + + 0.266672 + 0.537988 + 500.700012 + + + + 0.263409 + 0.541916 + 502.100006 + + + + 0.262681 + 0.543120 + 504.000000 + + + + 0.264231 + 0.542744 + 505.399994 + + + + 0.268529 + 0.539429 + 507.399994 + + + + 0.277662 + 0.528517 + 508.700012 + + + + 0.290994 + 0.510818 + 510.600006 + + + + 0.304901 + 0.489955 + 512.099976 + + + + 0.319566 + 0.467018 + 514.000000 + + + + 0.328959 + 0.453192 + 515.599976 + + + + 0.336279 + 0.444050 + 517.299988 + + + + 0.341794 + 0.440344 + 518.799988 + + + + 0.347328 + 0.439062 + 520.599976 + + + + 0.352925 + 0.438652 + 522.099976 + + + + 0.358917 + 0.438528 + 523.900024 + + + + 0.363028 + 0.438491 + 525.400024 + + + + 0.366784 + 0.438481 + 527.400024 + + + + 0.370847 + 0.438478 + 528.900024 + + + + 0.375878 + 0.439026 + 530.599976 + + + + 0.381314 + 0.441498 + 532.000000 + + + + 0.386464 + 0.444897 + 533.900024 + + + + 0.386990 + 0.446442 + 535.500000 + + + + 0.384857 + 0.447978 + 537.299988 + + + + 0.382292 + 0.452556 + 538.900024 + + + + 0.378701 + 0.460060 + 540.500000 + + + + 0.373160 + 0.469140 + 542.099976 + + + + 0.365731 + 0.479986 + 543.900024 + + + + 0.358456 + 0.489480 + 545.299988 + + + + 0.351377 + 0.498188 + 547.200012 + + + + 0.348293 + 0.501949 + 548.700012 + + + + 0.347986 + 0.501881 + 550.599976 + + + + 0.351199 + 0.495997 + 552.000000 + + + + 0.357159 + 0.485918 + 553.900024 + + + + 0.364576 + 0.475143 + 555.400024 + + + + 0.373101 + 0.463488 + 557.200012 + + + + 0.379096 + 0.455161 + 558.700012 + + + + 0.384186 + 0.448551 + 560.599976 + + + + 0.388065 + 0.445767 + 562.000000 + + + + 0.391945 + 0.444605 + 563.900024 + + + + 0.395839 + 0.443465 + 565.299988 + + + + 0.400169 + 0.442419 + 567.200012 + + + + 0.403799 + 0.442699 + 568.599976 + + + + 0.407846 + 0.444004 + 570.599976 + + + + 0.412816 + 0.446104 + 572.000000 + + + + 0.419199 + 0.449147 + 573.900024 + + + + 0.425986 + 0.452979 + 575.299988 + + + + 0.432459 + 0.457329 + 577.200012 + + + + 0.433563 + 0.459997 + 578.599976 + + + + 0.431635 + 0.462402 + 580.500000 + + + + 0.429137 + 0.465879 + 582.000000 + + + + 0.425567 + 0.471068 + 583.799988 + + + + 0.420032 + 0.478084 + 585.299988 + + + + 0.412971 + 0.487249 + 587.200012 + + + + 0.407346 + 0.496765 + 588.599976 + + + + 0.402533 + 0.506320 + 590.500000 + + + + 0.400478 + 0.510503 + 591.900024 + + + + 0.400361 + 0.511448 + 593.799988 + + + + 0.402873 + 0.509462 + 595.200012 + + + + 0.407921 + 0.504809 + 597.099976 + + + + 0.415658 + 0.496991 + 598.599976 + + + + 0.425742 + 0.486598 + 600.500000 + + + + 0.434939 + 0.477252 + 601.900024 + + + + 0.444523 + 0.468223 + 603.799988 + + + + 0.452646 + 0.462828 + 605.299988 + + + + 0.459832 + 0.459413 + 607.099976 + + + + 0.462184 + 0.458776 + 608.599976 + + + + 0.461997 + 0.460562 + 610.400024 + + + + 0.461023 + 0.466134 + 611.900024 + + + + 0.459022 + 0.476376 + 613.900024 + + + + 0.454908 + 0.492869 + 615.200012 + + + + 0.449254 + 0.515733 + 617.099976 + + + + 0.444486 + 0.540295 + 618.599976 + + + + 0.440625 + 0.567259 + 620.400024 + + + + 0.440457 + 0.587020 + 621.900024 + + + + 0.443666 + 0.604370 + 623.799988 + + + + 0.451628 + 0.617181 + 625.200012 + + + + 0.463893 + 0.627507 + 627.099976 + + + + 0.478073 + 0.630404 + 628.599976 + + + + 0.495688 + 0.626996 + 630.599976 + + + + 0.514596 + 0.614125 + 631.799988 + + + + 0.536437 + 0.592826 + 633.799988 + + + + 0.556186 + 0.566624 + 635.200012 + + + + 0.574705 + 0.538497 + 637.099976 + + + + 0.584683 + 0.521737 + 638.500000 + + + + 0.596327 + 0.497782 + 640.400024 + + + + 0.609338 + 0.469517 + 641.900024 + + + + 0.616886 + 0.450656 + 643.700012 + + + + 0.626927 + 0.418090 + 645.200012 + + + + 0.638739 + 0.374543 + 647.099976 + + + + 0.644749 + 0.340645 + 648.500000 + + + + 0.647749 + 0.312258 + 650.400024 + + + + 0.648873 + 0.300183 + 651.799988 + + + + 0.649248 + 0.295919 + 653.700012 + + + + 0.649365 + 0.294543 + 655.099976 + + + + 0.649400 + 0.294121 + 657.000000 + + + + 0.649410 + 0.293995 + 658.500000 + + + + +
+
diff --git a/generators/160.gml b/generators/160.gml new file mode 100755 index 0000000..ec75b4d --- /dev/null +++ b/generators/160.gml @@ -0,0 +1,2791 @@ + + +
+ + katsu-2f + +
+ + + -390.000000 + -32.000000 + 0.419999 + + + 27.000000 + 1.000000 + 0.000000 + + + + + + 0.147498 + 0.219538 + 0.000000 + + + + 0.147101 + 0.212942 + 4.800000 + + + + 0.146946 + 0.207503 + 8.100000 + + + + 0.146896 + 0.203511 + 11.700000 + + + + 0.146724 + 0.200267 + 14.900000 + + + + 0.145868 + 0.198226 + 21.400000 + + + + 0.144555 + 0.198876 + 37.400002 + + + + 0.144007 + 0.204583 + 40.700001 + + + + 0.143825 + 0.218993 + 43.799999 + + + + 0.144083 + 0.244724 + 46.799999 + + + + 0.145928 + 0.283239 + 50.000000 + + + + 0.149557 + 0.334128 + 55.200001 + + + + 0.153129 + 0.393071 + 58.299999 + + + + 0.157055 + 0.452832 + 61.500000 + + + + 0.160675 + 0.504902 + 64.699997 + + + + 0.162888 + 0.543471 + 68.000000 + + + + 0.164641 + 0.566973 + 70.800003 + + + + 0.165319 + 0.577339 + 74.099998 + + + + 0.165380 + 0.577548 + 77.500000 + + + + 0.164436 + 0.569779 + 80.699997 + + + + 0.162290 + 0.551430 + 83.800003 + + + + 0.160746 + 0.519498 + 86.900002 + + + + 0.162346 + 0.474171 + 90.099998 + + + + 0.168769 + 0.419902 + 95.099998 + + + + 0.179235 + 0.363529 + 96.599998 + + + + 0.191592 + 0.310718 + 101.600006 + + + + 0.203349 + 0.266803 + 104.599998 + + + + 0.212709 + 0.234213 + 107.800003 + + + + 0.221958 + 0.211743 + 111.099998 + + + + 0.230964 + 0.197793 + 114.500000 + + + + 0.238148 + 0.190139 + 117.699997 + + + + 0.241712 + 0.187392 + 121.199997 + + + + 0.241863 + 0.188725 + 127.599998 + + + + 0.238471 + 0.193682 + 131.100006 + + + + 0.230219 + 0.204596 + 134.399994 + + + + 0.215112 + 0.223009 + 137.600006 + + + + 0.190758 + 0.248874 + 140.899994 + + + + 0.160085 + 0.277562 + 144.000000 + + + + 0.137657 + 0.299022 + 147.199997 + + + + 0.128785 + 0.309744 + 150.600006 + + + + 0.133414 + 0.311778 + 154.100006 + + + + 0.155941 + 0.309188 + 157.199997 + + + + 0.193346 + 0.305108 + 160.500000 + + + + 0.228740 + 0.299964 + 163.800003 + + + + 0.253422 + 0.295978 + 167.199997 + + + + 0.264634 + 0.294442 + 170.399994 + + + + 0.266414 + 0.294263 + 175.100006 + + + + 0.263061 + 0.296915 + 178.300003 + + + + 0.252713 + 0.307180 + 188.000000 + + + + 0.232599 + 0.328229 + 191.399994 + + + + 0.204065 + 0.355782 + 194.500000 + + + + 0.172543 + 0.382441 + 197.800003 + + + + 0.149473 + 0.403684 + 201.000000 + + + + 0.140979 + 0.415348 + 204.199997 + + + + 0.147685 + 0.417817 + 207.199997 + + + + 0.170972 + 0.415219 + 210.300003 + + + + 0.207678 + 0.410333 + 213.600006 + + + + 0.241156 + 0.403733 + 218.399994 + + + + 0.264855 + 0.398507 + 220.199997 + + + + 0.276658 + 0.396329 + 224.699997 + + + + 0.278753 + 0.396704 + 227.899994 + + + + 0.275512 + 0.399013 + 231.399994 + + + + 0.265043 + 0.409096 + 240.699997 + + + + 0.244400 + 0.431799 + 244.000000 + + + + 0.215947 + 0.461355 + 247.199997 + + + + 0.184644 + 0.490551 + 250.399994 + + + + 0.158855 + 0.515649 + 255.300003 + + + + 0.147252 + 0.530952 + 258.600006 + + + + 0.150665 + 0.537521 + 260.200012 + + + + 0.170124 + 0.539555 + 265.299988 + + + + 0.203710 + 0.539361 + 268.500000 + + + + 0.237539 + 0.538235 + 270.200012 + + + + 0.263426 + 0.537737 + 275.000000 + + + + 0.276508 + 0.537570 + 278.100006 + + + + 0.280789 + 0.537051 + 281.399994 + + + + 0.282061 + 0.533388 + 291.299988 + + + + 0.280388 + 0.521766 + 294.600006 + + + + 0.276759 + 0.498281 + 297.799988 + + + + 0.271772 + 0.458763 + 301.299988 + + + + 0.267678 + 0.405435 + 304.100006 + + + + 0.265460 + 0.349976 + 307.399994 + + + + 0.264354 + 0.299561 + 310.500000 + + + + 0.263968 + 0.259491 + 313.700012 + + + + 0.261974 + 0.226793 + 317.000000 + + + + 0.259377 + 0.203145 + 321.799988 + + + + 0.257215 + 0.188513 + 323.500000 + + + + 0.255590 + 0.179361 + 328.299988 + + + + 0.253874 + 0.172849 + 331.600006 + + + + 0.252245 + 0.170845 + 334.700012 + + + + 0.250191 + 0.177144 + 344.500000 + + + + 0.251849 + 0.191016 + 347.899994 + + + + 0.264729 + 0.207100 + 350.799988 + + + + 0.286535 + 0.215832 + 354.100006 + + + + 0.305704 + 0.211457 + 357.700012 + + + + 0.322146 + 0.198270 + 360.600006 + + + + 0.332962 + 0.184742 + 363.899994 + + + + 0.337163 + 0.177908 + 367.000000 + + + + 0.338044 + 0.180978 + 371.899994 + + + + 0.341200 + 0.195514 + 375.299988 + + + + 0.348981 + 0.221432 + 378.700012 + + + + 0.358888 + 0.249712 + 381.799988 + + + + 0.368269 + 0.268955 + 383.500000 + + + + 0.375277 + 0.277451 + 388.299988 + + + + 0.376711 + 0.274843 + 391.600006 + + + + 0.370444 + 0.264650 + 394.700012 + + + + 0.354739 + 0.251913 + 397.899994 + + + + 0.330954 + 0.239503 + 401.100037 + + + + 0.305098 + 0.234869 + 404.400024 + + + + 0.278715 + 0.242935 + 407.799988 + + + + 0.257355 + 0.258671 + 411.299988 + + + + 0.247030 + 0.276599 + 414.799988 + + + + 0.249702 + 0.292942 + 418.100006 + + + + 0.269077 + 0.306275 + 421.299988 + + + + 0.298912 + 0.313020 + 424.500000 + + + + 0.324806 + 0.311014 + 427.700012 + + + + 0.347155 + 0.301181 + 431.200012 + + + + 0.364667 + 0.289043 + 434.299988 + + + + 0.371274 + 0.284698 + 437.600006 + + + + 0.369309 + 0.291631 + 440.600006 + + + + 0.366688 + 0.310593 + 443.799988 + + + + 0.369982 + 0.336748 + 447.000000 + + + + 0.377064 + 0.359132 + 451.799988 + + + + 0.382586 + 0.371178 + 453.399994 + + + + 0.384248 + 0.372355 + 458.200012 + + + + 0.379720 + 0.364313 + 461.500000 + + + + 0.368068 + 0.349230 + 464.700012 + + + + 0.350863 + 0.332844 + 467.899994 + + + + 0.330266 + 0.323100 + 471.000000 + + + + 0.306545 + 0.322503 + 474.200012 + + + + 0.279713 + 0.334284 + 477.500000 + + + + 0.258044 + 0.354927 + 480.799988 + + + + 0.251509 + 0.375972 + 485.399994 + + + + 0.259964 + 0.394879 + 487.000000 + + + + 0.283940 + 0.408116 + 491.899994 + + + + 0.315556 + 0.415470 + 495.200012 + + + + 0.343272 + 0.413118 + 498.600006 + + + + 0.364788 + 0.402943 + 502.000000 + + + + 0.378398 + 0.391449 + 505.000000 + + + + 0.383290 + 0.386474 + 508.299988 + + + + 0.382467 + 0.390747 + 511.399994 + + + + 0.381383 + 0.405106 + 514.599976 + + + + 0.385468 + 0.429323 + 518.000000 + + + + 0.393922 + 0.458409 + 521.099976 + + + + 0.402835 + 0.479974 + 524.400024 + + + + 0.409548 + 0.489318 + 527.599976 + + + + 0.409624 + 0.485882 + 530.799988 + + + + 0.399947 + 0.472618 + 534.000000 + + + + 0.382151 + 0.456040 + 537.299988 + + + + 0.359939 + 0.441743 + 540.500000 + + + + 0.335052 + 0.437076 + 545.400024 + + + + 0.308402 + 0.446628 + 548.500000 + + + + 0.282966 + 0.466629 + 551.799988 + + + + 0.268008 + 0.490933 + 554.900024 + + + + 0.264737 + 0.518682 + 558.000000 + + + + 0.277294 + 0.545423 + 561.200012 + + + + 0.304358 + 0.563725 + 564.599976 + + + + 0.336221 + 0.565624 + 567.900024 + + + + 0.365144 + 0.554470 + 571.099976 + + + + 0.385629 + 0.537324 + 574.299988 + + + + 0.395120 + 0.520635 + 577.400024 + + + + 0.397741 + 0.509295 + 580.700012 + + + + 0.398446 + 0.504870 + 585.500000 + + + + 0.400507 + 0.501883 + 595.200012 + + + + 0.403275 + 0.494354 + 598.299988 + + + + 0.405980 + 0.476584 + 601.599976 + + + + 0.407215 + 0.447245 + 604.700012 + + + + 0.406229 + 0.409150 + 608.000000 + + + + 0.402911 + 0.365193 + 611.099976 + + + + 0.395999 + 0.320598 + 614.400024 + + + + 0.387898 + 0.283510 + 617.599976 + + + + 0.378035 + 0.255143 + 620.799988 + + + + 0.364471 + 0.234389 + 625.500000 + + + + 0.348860 + 0.220218 + 627.200012 + + + + 0.333607 + 0.213004 + 630.900024 + + + + 0.323149 + 0.210466 + 635.500000 + + + + 0.319674 + 0.209528 + 637.200012 + + + + 0.324884 + 0.208452 + 641.000000 + + + + 0.344589 + 0.207077 + 644.299988 + + + + 0.381633 + 0.206356 + 647.400024 + + + + 0.428256 + 0.205475 + 650.599976 + + + + 0.474102 + 0.204958 + 655.599976 + + + + 0.505527 + 0.205708 + 658.799988 + + + + 0.516439 + 0.205758 + 661.799988 + + + + 0.508215 + 0.203712 + 665.000000 + + + + 0.485687 + 0.198101 + 668.500000 + + + + 0.458773 + 0.190210 + 671.700012 + + + + 0.437007 + 0.182408 + 674.900024 + + + + 0.424183 + 0.176636 + 678.299988 + + + + 0.419345 + 0.173647 + 681.599976 + + + + 0.418746 + 0.173684 + 684.799988 + + + + 0.418562 + 0.176975 + 694.599976 + + + + 0.417728 + 0.184705 + 697.799988 + + + + 0.416737 + 0.200831 + 701.099976 + + + + 0.418364 + 0.231608 + 704.200012 + + + + 0.423521 + 0.276369 + 707.400024 + + + + 0.429823 + 0.327786 + 710.599976 + + + + 0.435994 + 0.385716 + 715.400024 + + + + 0.443300 + 0.443433 + 718.599976 + + + + 0.450654 + 0.494634 + 721.799988 + + + + 0.457348 + 0.534476 + 725.000000 + + + + 0.461711 + 0.558751 + 728.200012 + + + + 0.464278 + 0.568464 + 731.599976 + + + + 0.465058 + 0.568496 + 738.000000 + + + + 0.464499 + 0.562244 + 741.500000 + + + + 0.463116 + 0.550366 + 744.900024 + + + + 0.461115 + 0.531740 + 748.299988 + + + + 0.455752 + 0.504265 + 751.299988 + + + + 0.446720 + 0.470332 + 754.500000 + + + + 0.436830 + 0.434405 + 758.000000 + + + + 0.427988 + 0.400716 + 761.400024 + + + + 0.421847 + 0.373602 + 764.700012 + + + + 0.416401 + 0.354023 + 768.000000 + + + + 0.410507 + 0.340942 + 771.299988 + + + + 0.402502 + 0.332598 + 774.500000 + + + + 0.393569 + 0.327622 + 778.000000 + + + + 0.384325 + 0.325644 + 781.200012 + + + + 0.375299 + 0.324378 + 784.700012 + + + + 0.368262 + 0.323594 + 787.700012 + + + + 0.366172 + 0.323617 + 790.900024 + + + + 0.372145 + 0.323510 + 799.000000 + + + + 0.389782 + 0.324242 + 800.599976 + + + + 0.417888 + 0.323640 + 805.400024 + + + + 0.452310 + 0.321457 + 808.700012 + + + + 0.488739 + 0.318786 + 812.199951 + + + + 0.514817 + 0.316756 + 815.499939 + + + + 0.525668 + 0.316132 + 818.700073 + + + + 0.525497 + 0.316734 + 822.000000 + + + + 0.520815 + 0.318286 + 825.299988 + + + + 0.512779 + 0.321455 + 828.700012 + + + + 0.499554 + 0.330459 + 831.799988 + + + + 0.480254 + 0.346696 + 835.099976 + + + + 0.457230 + 0.366834 + 838.500000 + + + + 0.435334 + 0.385397 + 841.599976 + + + + 0.417258 + 0.398212 + 844.799988 + + + + 0.404593 + 0.407738 + 848.000000 + + + + 0.394486 + 0.414978 + 851.200012 + + + + 0.387133 + 0.420416 + 854.799988 + + + + 0.384329 + 0.423438 + 858.099976 + + + + 0.386247 + 0.424527 + 861.299988 + + + + 0.395220 + 0.424865 + 867.799988 + + + + 0.415724 + 0.424963 + 870.900024 + + + + 0.446690 + 0.424990 + 874.099976 + + + + 0.483037 + 0.425154 + 878.900024 + + + + 0.513626 + 0.426007 + 882.200012 + + + + 0.530573 + 0.427008 + 885.500000 + + + + 0.536514 + 0.425072 + 891.900024 + + + + 0.539590 + 0.417120 + 898.299988 + + + + 0.542084 + 0.400982 + 901.900024 + + + + 0.544145 + 0.375992 + 905.200012 + + + + 0.544324 + 0.343366 + 908.700012 + + + + 0.546176 + 0.307746 + 911.900024 + + + + 0.550750 + 0.274911 + 915.099976 + + + + 0.556467 + 0.247385 + 918.299988 + + + + 0.562560 + 0.226640 + 921.900024 + + + + 0.568610 + 0.213369 + 925.099976 + + + + 0.573840 + 0.206559 + 928.299988 + + + + 0.577922 + 0.204314 + 932.000000 + + + + 0.581998 + 0.204738 + 935.200012 + + + + 0.585659 + 0.207542 + 938.400024 + + + + 0.587884 + 0.210501 + 941.599976 + + + + 0.589484 + 0.210442 + 944.900024 + + + + 0.589155 + 0.206517 + 952.200012 + + + + 0.586739 + 0.198973 + 955.500000 + + + + 0.581450 + 0.189525 + 958.799988 + + + + 0.570753 + 0.180337 + 962.000000 + + + + 0.554798 + 0.175269 + 965.299988 + + + + 0.536278 + 0.176573 + 968.400024 + + + + 0.517960 + 0.185382 + 971.700012 + + + + 0.504393 + 0.202094 + 974.900024 + + + + 0.497798 + 0.226673 + 978.700012 + + + + 0.498790 + 0.251586 + 982.000000 + + + + 0.508159 + 0.266724 + 985.099976 + + + + 0.524045 + 0.272603 + 988.700012 + + + + 0.540881 + 0.268866 + 992.000000 + + + + 0.554694 + 0.257203 + 995.099976 + + + + 0.568662 + 0.241204 + 998.400024 + + + + 0.586445 + 0.224275 + 1001.599976 + + + + 0.607029 + 0.214020 + 1004.799988 + + + + 0.628998 + 0.214627 + 1008.099976 + + + + 0.649572 + 0.225100 + 1011.299988 + + + + 0.663201 + 0.244939 + 1014.599976 + + + + 0.665337 + 0.270374 + 1017.799988 + + + + 0.654835 + 0.294422 + 1021.000000 + + + + 0.636253 + 0.310065 + 1025.800049 + + + + 0.613572 + 0.316058 + 1029.000000 + + + + 0.592576 + 0.311410 + 1032.300049 + + + + 0.576137 + 0.297925 + 1035.599976 + + + + 0.566508 + 0.282871 + 1038.699951 + + + + 0.564403 + 0.274740 + 1041.900024 + + + + 0.570343 + 0.280583 + 1048.400024 + + + + 0.581247 + 0.298306 + 1051.599976 + + + + 0.591481 + 0.320678 + 1054.800049 + + + + 0.599135 + 0.340327 + 1058.000000 + + + + 0.604412 + 0.351957 + 1061.199951 + + + + 0.605622 + 0.351208 + 1064.400024 + + + + 0.602778 + 0.339352 + 1067.699951 + + + + 0.597219 + 0.322804 + 1070.900024 + + + + 0.586266 + 0.306888 + 1075.699951 + + + + 0.569584 + 0.293991 + 1077.500000 + + + + 0.550593 + 0.289608 + 1082.300049 + + + + 0.532725 + 0.297187 + 1085.500000 + + + + 0.517822 + 0.312066 + 1088.699951 + + + + 0.508728 + 0.331508 + 1091.900024 + + + + 0.508349 + 0.357325 + 1095.199951 + + + + 0.515664 + 0.380783 + 1098.300049 + + + + 0.528819 + 0.394893 + 1101.599976 + + + + 0.544529 + 0.398818 + 1104.800049 + + + + 0.557503 + 0.392380 + 1107.900024 + + + + 0.568650 + 0.379443 + 1111.099976 + + + + 0.581767 + 0.362380 + 1114.500000 + + + + 0.599613 + 0.344568 + 1117.699951 + + + + 0.618550 + 0.333499 + 1122.500000 + + + + 0.635398 + 0.332690 + 1126.099976 + + + + 0.651876 + 0.343348 + 1128.199951 + + + + 0.664543 + 0.362404 + 1132.000000 + + + + 0.669732 + 0.385758 + 1136.000000 + + + + 0.665965 + 0.407815 + 1138.000000 + + + + 0.655260 + 0.421885 + 1142.000000 + + + + 0.641100 + 0.426645 + 1145.900024 + + + + 0.623389 + 0.423083 + 1147.900024 + + + + 0.605345 + 0.412180 + 1151.900024 + + + + 0.591861 + 0.398389 + 1155.900024 + + + + 0.584977 + 0.387573 + 1159.900024 + + + + 0.583646 + 0.382926 + 1161.900024 + + + + 0.586361 + 0.386844 + 1165.900024 + + + + 0.592024 + 0.400871 + 1170.000000 + + + + 0.598187 + 0.422768 + 1172.099976 + + + + 0.602552 + 0.444890 + 1176.099976 + + + + 0.605770 + 0.459687 + 1180.000000 + + + + 0.607000 + 0.461333 + 1182.000000 + + + + 0.605205 + 0.452585 + 1186.000000 + + + + 0.600118 + 0.437472 + 1190.000000 + + + + 0.589793 + 0.419546 + 1192.000000 + + + + 0.576026 + 0.405203 + 1196.000000 + + + + 0.562378 + 0.399333 + 1200.099976 + + + + 0.546925 + 0.403189 + 1202.199951 + + + + 0.530029 + 0.416156 + 1206.199951 + + + + 0.517886 + 0.436955 + 1208.300049 + + + + 0.513735 + 0.464572 + 1212.400024 + + + + 0.517474 + 0.491428 + 1216.400024 + + + + 0.528895 + 0.509789 + 1218.500000 + + + + 0.545723 + 0.517334 + 1222.500000 + + + + 0.563818 + 0.513873 + 1226.500000 + + + + 0.580026 + 0.502408 + 1228.599976 + + + + 0.596883 + 0.485546 + 1232.800049 + + + + 0.617096 + 0.468063 + 1234.900024 + + + + 0.636912 + 0.456976 + 1239.099976 + + + + 0.653880 + 0.457392 + 1241.199951 + + + + 0.669432 + 0.470322 + 1245.300049 + + + + 0.679957 + 0.493150 + 1249.300049 + + + + 0.683290 + 0.520121 + 1251.500000 + + + + 0.678327 + 0.546741 + 1255.599976 + + + + 0.664051 + 0.567840 + 1259.800049 + + + + 0.641157 + 0.580379 + 1261.900024 + + + + 0.612037 + 0.582622 + 1266.099976 + + + + 0.583202 + 0.576130 + 1268.300049 + + + + 0.561058 + 0.562159 + 1272.500000 + + + + 0.550816 + 0.545103 + 1274.599976 + + + + 0.549971 + 0.530313 + 1278.900024 + + + + 0.552603 + 0.521073 + 1283.199951 + + + + 0.554454 + 0.518592 + 1285.400024 + + + + 0.557187 + 0.515750 + 1289.699951 + + + + 0.565511 + 0.504439 + 1295.599976 + + + + 0.581152 + 0.483085 + 1300.099976 + + + + 0.606212 + 0.451072 + 1302.599976 + + + + 0.633582 + 0.414297 + 1304.900024 + + + + 0.653239 + 0.379192 + 1309.199951 + + + + 0.665091 + 0.347578 + 1311.500000 + + + + 0.667723 + 0.320888 + 1315.800049 + + + + 0.665502 + 0.296552 + 1320.099976 + + + + 0.662496 + 0.273029 + 1322.300049 + + + + 0.659536 + 0.251629 + 1326.599976 + + + + 0.657260 + 0.233176 + 1328.800049 + + + + 0.655603 + 0.218285 + 1333.300049 + + + + 0.654033 + 0.205944 + 1335.500000 + + + + 0.653254 + 0.193948 + 1339.900024 + + + + 0.652200 + 0.184011 + 1342.099976 + + + + 0.650984 + 0.179514 + 1346.500000 + + + + 0.651583 + 0.182362 + 1348.800049 + + + + 0.654725 + 0.194181 + 1353.199951 + + + + 0.659547 + 0.217951 + 1355.500000 + + + + 0.663591 + 0.250556 + 1359.900024 + + + + 0.665951 + 0.280603 + 1362.099976 + + + + 0.667748 + 0.301399 + 1366.400024 + + + + 0.668439 + 0.313631 + 1368.599976 + + + + 0.669129 + 0.318251 + 1373.000000 + + + + 0.672842 + 0.318625 + 1375.199951 + + + + 0.684166 + 0.317937 + 1386.199951 + + + + 0.704545 + 0.317297 + 1388.500000 + + + + 0.732368 + 0.317199 + 1392.900024 + + + + 0.762776 + 0.316406 + 1395.099976 + + + + 0.786474 + 0.316835 + 1399.599976 + + + + 0.798431 + 0.317537 + 1401.800049 + + + + 0.800595 + 0.314393 + 1408.400024 + + + + 0.798315 + 0.303256 + 1412.900024 + + + + 0.794056 + 0.283246 + 1415.199951 + + + + 0.788816 + 0.257298 + 1419.599976 + + + + 0.782921 + 0.230216 + 1421.900024 + + + + 0.778655 + 0.207764 + 1426.400024 + + + + 0.775311 + 0.193858 + 1428.699951 + + + + 0.773238 + 0.190285 + 1433.199951 + + + + 0.773410 + 0.197498 + 1435.699951 + + + + 0.775310 + 0.216704 + 1439.300049 + + + + 0.777407 + 0.248267 + 1442.599976 + + + + 0.780755 + 0.289166 + 1445.900024 + + + + 0.789189 + 0.333961 + 1449.199951 + + + + 0.800941 + 0.377074 + 1452.500000 + + + + 0.809557 + 0.410329 + 1455.800049 + + + + 0.814410 + 0.432982 + 1459.099976 + + + + 0.816319 + 0.444169 + 1462.500000 + + + + 0.816154 + 0.447289 + 1465.900024 + + + + 0.816609 + 0.447197 + 1469.199951 + + + + 0.816510 + 0.446843 + 1475.800049 + + + + 0.812068 + 0.445907 + 1479.099976 + + + + 0.800386 + 0.444410 + 1482.400024 + + + + 0.780039 + 0.443002 + 1485.699951 + + + + 0.753186 + 0.441818 + 1489.099976 + + + + 0.724619 + 0.443230 + 1492.400024 + + + + 0.700763 + 0.447363 + 1495.800049 + + + + 0.685206 + 0.451027 + 1499.099976 + + + + 0.679398 + 0.452469 + 1502.500000 + + + + 0.678969 + 0.452466 + 1505.800049 + + + + 0.678285 + 0.449268 + 1509.100098 + + + + 0.675342 + 0.439860 + 1512.599976 + + + + 0.673114 + 0.423442 + 1515.899902 + + + + 0.671947 + 0.400587 + 1519.300049 + + + + 0.669814 + 0.374874 + 1522.699951 + + + + 0.666257 + 0.352257 + 1526.100098 + + + + 0.663825 + 0.339772 + 1529.499878 + + + + 0.663065 + 0.339035 + 1532.900024 + + + + 0.663938 + 0.349327 + 1536.300049 + + + + 0.667181 + 0.372746 + 1538.000000 + + + + 0.672035 + 0.406363 + 1542.999878 + + + + 0.676244 + 0.448289 + 1546.499878 + + + + 0.679614 + 0.494068 + 1548.199951 + + + + 0.683577 + 0.531947 + 1551.699951 + + + + 0.686129 + 0.560923 + 1555.099976 + + + + 0.687079 + 0.580349 + 1558.500000 + + + + 0.687379 + 0.590142 + 1561.900024 + + + + 0.687467 + 0.594358 + 1565.400024 + + + + 0.687493 + 0.597168 + 1579.099854 + + + + 0.687655 + 0.599945 + 1582.599854 + + + + 0.688820 + 0.601836 + 1586.000000 + + + + 0.693243 + 0.601767 + 1598.000000 + + + + 0.705331 + 0.600703 + 1603.099854 + + + + 0.727112 + 0.600228 + 1606.600220 + + + + 0.754746 + 0.600224 + 1608.400024 + + + + 0.786093 + 0.601027 + 1611.899902 + + + + 0.813192 + 0.602482 + 1615.499878 + + + + 0.830850 + 0.603878 + 1618.199951 + + + + 0.838535 + 0.605215 + 1622.900024 + + + + 0.840017 + 0.604340 + 1625.400146 + + + + 0.839206 + 0.598724 + 1637.400146 + + + + 0.835954 + 0.585459 + 1640.000244 + + + + 0.830625 + 0.564145 + 1642.500000 + + + + 0.823858 + 0.538030 + 1647.400024 + + + + 0.816393 + 0.510937 + 1649.900024 + + + + 0.809934 + 0.485051 + 1652.399780 + + + + 0.805526 + 0.467726 + 1657.300171 + + + + 0.803709 + 0.462162 + 1659.800049 + + + + 0.802018 + 0.463961 + 1667.199951 + + + + +
+
diff --git a/generators/OSC3.py b/generators/OSC3.py new file mode 100644 index 0000000..f0df277 --- /dev/null +++ b/generators/OSC3.py @@ -0,0 +1,2873 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +March 2015: + Python 3 version tested in Blender and simpleOSC with twisted + +This module contains an OpenSoundControl implementation (in Pure Python), based +(somewhat) on the good old 'SimpleOSC' implementation by Daniel Holth & Clinton +McChesney. + +This implementation is intended to still be 'simple' to the user, but much more +complete (with OSCServer & OSCClient classes) and much more powerful (the +OSCMultiClient supports subscriptions & message-filtering, OSCMessage & +OSCBundle are now proper container-types) + +=============================================================================== +OpenSoundControl +=============================================================================== + +OpenSoundControl is a network-protocol for sending (small) packets of addressed +data over network sockets. This OSC-implementation supports the classical +UDP/IP protocol for sending and receiving packets but provides as well support +for TCP/IP streaming, whereas the message size is prepended as int32 (big +endian) before each message/packet. + +OSC-packets come in two kinds: + + - OSC-messages consist of an 'address'-string (not to be confused with a + (host:port) network-address!), followed by a string of 'typetags' + associated with the message's arguments (ie. 'payload'), and finally the + arguments themselves, encoded in an OSC-specific way. The OSCMessage class + makes it easy to create & manipulate OSC-messages of this kind in a + 'pythonesque' way (that is, OSCMessage-objects behave a lot like lists) + + - OSC-bundles are a special type of OSC-message containing only + OSC-messages as 'payload'. Recursively. (meaning; an OSC-bundle could + contain other OSC-bundles, containing OSC-bundles etc.) + +OSC-bundles start with the special keyword '#bundle' and do not have an +OSC-address (but the OSC-messages a bundle contains will have OSC-addresses!). +Also, an OSC-bundle can have a timetag, essentially telling the receiving +server to 'hold' the bundle until the specified time. The OSCBundle class +allows easy cration & manipulation of OSC-bundles. + +For further information see also http://opensoundcontrol.org/spec-1_0 + +------------------------------------------------------------------------------- + +To send OSC-messages, you need an OSCClient, and to receive OSC-messages you +need an OSCServer. + +The OSCClient uses an 'AF_INET / SOCK_DGRAM' type socket (see the 'socket' +module) to send binary representations of OSC-messages to a remote host:port +address. + +The OSCServer listens on an 'AF_INET / SOCK_DGRAM' type socket bound to a local +port, and handles incoming requests. Either one-after-the-other (OSCServer) or +in a multi-threaded / multi-process fashion (ThreadingOSCServer/ +ForkingOSCServer). If the Server has a callback-function (a.k.a. handler) +registered to 'deal with' (i.e. handle) the received message's OSC-address, +that function is called, passing it the (decoded) message. + +The different OSCServers implemented here all support the (recursive) un- +bundling of OSC-bundles, and OSC-bundle timetags. + +In fact, this implementation supports: + + - OSC-messages with 'i' (int32), 'f' (float32), 'd' (double), 's' (string) and + 'b' (blob / binary data) types + - OSC-bundles, including timetag-support + - OSC-address patterns including '*', '?', '{,}' and '[]' wildcards. + +(please *do* read the OSC-spec! http://opensoundcontrol.org/spec-1_0 it +explains what these things mean.) + +In addition, the OSCMultiClient supports: + - Sending a specific OSC-message to multiple remote servers + - Remote server subscription / unsubscription (through OSC-messages, of course) + - Message-address filtering. + +------------------------------------------------------------------------------- +SimpleOSC: + Copyright (c) Daniel Holth & Clinton McChesney. +pyOSC: + Copyright (c) 2008-2010, Artem Baguinski et al., Stock, V2_Lab, Rotterdam, Netherlands. +Streaming support (OSC over TCP): + Copyright (c) 2010 Uli Franke , Weiss Engineering, Uster, Switzerland. + +------------------------------------------------------------------------------- +Changelog: +------------------------------------------------------------------------------- +v0.3.0 - 27 Dec. 2007 + Started out to extend the 'SimpleOSC' implementation (v0.2.3) by Daniel Holth & Clinton McChesney. + Rewrote OSCMessage + Added OSCBundle + +v0.3.1 - 3 Jan. 2008 + Added OSClient + Added OSCRequestHandler, loosely based on the original CallbackManager + Added OSCServer + Removed original CallbackManager + Adapted testing-script (the 'if __name__ == "__main__":' block at the end) to use new Server & Client + +v0.3.2 - 5 Jan. 2008 + Added 'container-type emulation' methods (getitem(), setitem(), __iter__() & friends) to OSCMessage + Added ThreadingOSCServer & ForkingOSCServer + - 6 Jan. 2008 + Added OSCMultiClient + Added command-line options to testing-script (try 'python OSC.py --help') + +v0.3.3 - 9 Jan. 2008 + Added OSC-timetag support to OSCBundle & OSCRequestHandler + Added ThreadingOSCRequestHandler + +v0.3.4 - 13 Jan. 2008 + Added message-filtering to OSCMultiClient + Added subscription-handler to OSCServer + Added support fon numpy/scipy int & float types. (these get converted to 'standard' 32-bit OSC ints / floats!) + Cleaned-up and added more Docstrings + +v0.3.5 - 14 aug. 2008 + Added OSCServer.reportErr(...) method + +v0.3.6 - 19 April 2010 + Added Streaming support (OSC over TCP) + Updated documentation + Moved pattern matching stuff into separate class (OSCAddressSpace) to + facilitate implementation of different server and client architectures. + Callbacks feature now a context (object oriented) but dynamic function + inspection keeps the code backward compatible + Moved testing code into separate testbench (testbench.py) + +----------------- +Original Comments +----------------- +> Open SoundControl for Python +> Copyright (C) 2002 Daniel Holth, Clinton McChesney +> +> This library is free software; you can redistribute it and/or modify it under +> the terms of the GNU Lesser General Public License as published by the Free +> Software Foundation; either version 2.1 of the License, or (at your option) any +> later version. +> +> This library is distributed in the hope that it will be useful, but WITHOUT ANY +> WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +> PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +> details. +> +> You should have received a copy of the GNU Lesser General Public License along +> with this library; if not, write to the Free Software Foundation, Inc., 59 +> Temple Place, Suite 330, Boston, MA 02111-1307 USA +> +> For questions regarding this module contact Daniel Holth +> or visit http://www.stetson.edu/~ProctoLogic/ +> +> Changelog: +> 15 Nov. 2001: +> Removed dependency on Python 2.0 features. +> - dwh +> 13 Feb. 2002: +> Added a generic callback handler. +> - dwh +""" + +import math, re, socket, select, string, struct, sys, threading, time, types, array, errno, inspect +from socketserver import UDPServer, DatagramRequestHandler, ForkingMixIn, ThreadingMixIn, StreamRequestHandler, TCPServer +from contextlib import closing + +global version +version = ("0.3","6", "$Rev: 6382 $"[6:-2]) + +global FloatTypes +FloatTypes = [float] + +global IntTypes +IntTypes = [int] + +global NTP_epoch +from calendar import timegm +NTP_epoch = timegm((1900,1,1,0,0,0)) # NTP time started in 1 Jan 1900 +del timegm + +global NTP_units_per_second +NTP_units_per_second = 0x100000000 # about 232 picoseconds + +## +# numpy/scipy support: +## + +try: + from numpy import typeDict + + for ftype in ['float32', 'float64', 'float128']: + try: + FloatTypes.append(typeDict[ftype]) + except KeyError: + pass + + for itype in ['int8', 'int16', 'int32', 'int64']: + try: + IntTypes.append(typeDict[itype]) + IntTypes.append(typeDict['u' + itype]) + except KeyError: + pass + + # thanks for those... + del typeDict, ftype, itype + +except ImportError: + pass + +###### +# +# OSCMessage classes +# +###### + +class OSCMessage(object): + """ Builds typetagged OSC messages. + + OSCMessage objects are container objects for building OSC-messages. + On the 'front' end, they behave much like list-objects, and on the 'back' end + they generate a binary representation of the message, which can be sent over a network socket. + OSC-messages consist of an 'address'-string (not to be confused with a (host, port) IP-address!), + followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'), + and finally the arguments themselves, encoded in an OSC-specific way. + + On the Python end, OSCMessage are lists of arguments, prepended by the message's address. + The message contents can be manipulated much like a list: + >>> msg = OSCMessage("/my/osc/address") + >>> msg.append('something') + >>> msg.insert(0, 'something else') + >>> msg[1] = 'entirely' + >>> msg.extend([1,2,3.]) + >>> msg += [4, 5, 6.] + >>> del msg[3:6] + >>> msg.pop(-2) + 5 + >>> print msg + /my/osc/address ['something else', 'entirely', 1, 6.0] + + OSCMessages can be concatenated with the + operator. In this case, the resulting OSCMessage + inherits its address from the left-hand operand. The right-hand operand's address is ignored. + To construct an 'OSC-bundle' from multiple OSCMessage, see OSCBundle! + + Additional methods exist for retreiving typetags or manipulating items as (typetag, value) tuples. + """ + def __init__(self, address=""): + """Instantiate a new OSCMessage. + The OSC-address can be specified with the 'address' argument + """ + self.clear(address) + + def setAddress(self, address): + """Set or change the OSC-address + """ + self.address = address + + def clear(self, address=""): + """Clear (or set a new) OSC-address and clear any arguments appended so far + """ + self.address = address + self.clearData() + + def clearData(self): + """Clear any arguments appended so far + """ + self.typetags = "," + self.message = b"" + + def append(self, argument, typehint=None): + """Appends data to the message, updating the typetags based on + the argument's type. If the argument is a blob (counted + string) pass in 'b' as typehint. + 'argument' may also be a list or tuple, in which case its elements + will get appended one-by-one, all using the provided typehint + """ + if isinstance(argument,dict): + argument = list(argument.items()) + elif isinstance(argument, OSCMessage): + raise TypeError("Can only append 'OSCMessage' to 'OSCBundle'") + + if hasattr(argument, '__iter__') and not type(argument) in (str,bytes): + for arg in argument: + self.append(arg, typehint) + + return + + if typehint == 'b': + binary = OSCBlob(argument) + tag = 'b' + elif typehint == 't': + binary = OSCTimeTag(argument) + tag = 't' + else: + tag, binary = OSCArgument(argument, typehint) + + self.typetags += tag + self.message += binary + + def getBinary(self): + """Returns the binary representation of the message + """ + binary = OSCString(self.address) + binary += OSCString(self.typetags) + binary += self.message + + return binary + + def __repr__(self): + """Returns a string containing the decode Message + """ + return str(decodeOSC(self.getBinary())) + + def __str__(self): + """Returns the Message's address and contents as a string. + """ + return "%s %s" % (self.address, str(list(self.values()))) + + def __len__(self): + """Returns the number of arguments appended so far + """ + return (len(self.typetags) - 1) + + def __eq__(self, other): + """Return True if two OSCMessages have the same address & content + """ + if not isinstance(other, self.__class__): + return False + + return (self.address == other.address) and (self.typetags == other.typetags) and (self.message == other.message) + + def __ne__(self, other): + """Return (not self.__eq__(other)) + """ + return not self.__eq__(other) + + def __add__(self, values): + """Returns a copy of self, with the contents of 'values' appended + (see the 'extend()' method, below) + """ + msg = self.copy() + msg.extend(values) + return msg + + def __iadd__(self, values): + """Appends the contents of 'values' + (equivalent to 'extend()', below) + Returns self + """ + self.extend(values) + return self + + def __radd__(self, values): + """Appends the contents of this OSCMessage to 'values' + Returns the extended 'values' (list or tuple) + """ + out = list(values) + out.extend(list(self.values())) + + if isinstance(values,tuple): + return tuple(out) + + return out + + def _reencode(self, items): + """Erase & rebuild the OSCMessage contents from the given + list of (typehint, value) tuples""" + self.clearData() + for item in items: + self.append(item[1], item[0]) + + def values(self): + """Returns a list of the arguments appended so far + """ + return decodeOSC(self.getBinary())[2:] + + def tags(self): + """Returns a list of typetags of the appended arguments + """ + return list(self.typetags.lstrip(',')) + + def items(self): + """Returns a list of (typetag, value) tuples for + the arguments appended so far + """ + out = [] + values = list(self.values()) + typetags = self.tags() + for i in range(len(values)): + out.append((typetags[i], values[i])) + + return out + + def __contains__(self, val): + """Test if the given value appears in the OSCMessage's arguments + """ + return (val in list(self.values())) + + def __getitem__(self, i): + """Returns the indicated argument (or slice) + """ + return list(self.values())[i] + + def __delitem__(self, i): + """Removes the indicated argument (or slice) + """ + items = list(self.items()) + del items[i] + + self._reencode(items) + + def _buildItemList(self, values, typehint=None): + if isinstance(values, OSCMessage): + items = list(values.items()) + elif isinstance(values,list): + items = [] + for val in values: + if isinstance(val,tuple): + items.append(val[:2]) + else: + items.append((typehint, val)) + elif isinstance(values,tuple): + items = [values[:2]] + else: + items = [(typehint, values)] + + return items + + def __setitem__(self, i, val): + """Set indicatated argument (or slice) to a new value. + 'val' can be a single int/float/string, or a (typehint, value) tuple. + Or, if 'i' is a slice, a list of these or another OSCMessage. + """ + items = list(self.items()) + + new_items = self._buildItemList(val) + + if not isinstance(i,slice): + if len(new_items) != 1: + raise TypeError("single-item assignment expects a single value or a (typetag, value) tuple") + + new_items = new_items[0] + + # finally... + items[i] = new_items + + self._reencode(items) + + def setItem(self, i, val, typehint=None): + """Set indicated argument to a new value (with typehint) + """ + items = list(self.items()) + + items[i] = (typehint, val) + + self._reencode(items) + + def copy(self): + """Returns a deep copy of this OSCMessage + """ + msg = self.__class__(self.address) + msg.typetags = self.typetags + msg.message = self.message + return msg + + def count(self, val): + """Returns the number of times the given value occurs in the OSCMessage's arguments + """ + return list(self.values()).count(val) + + def index(self, val): + """Returns the index of the first occurence of the given value in the OSCMessage's arguments. + Raises ValueError if val isn't found + """ + return list(self.values()).index(val) + + def extend(self, values): + """Append the contents of 'values' to this OSCMessage. + 'values' can be another OSCMessage, or a list/tuple of ints/floats/strings + """ + items = list(self.items()) + self._buildItemList(values) + + self._reencode(items) + + def insert(self, i, val, typehint = None): + """Insert given value (with optional typehint) into the OSCMessage + at the given index. + """ + items = list(self.items()) + + for item in reversed(self._buildItemList(val)): + items.insert(i, item) + + self._reencode(items) + + def popitem(self, i): + """Delete the indicated argument from the OSCMessage, and return it + as a (typetag, value) tuple. + """ + items = list(self.items()) + + item = items.pop(i) + + self._reencode(items) + + return item + + def pop(self, i): + """Delete the indicated argument from the OSCMessage, and return it. + """ + return self.popitem(i)[1] + + def reverse(self): + """Reverses the arguments of the OSCMessage (in place) + """ + items = list(self.items()) + + items.reverse() + + self._reencode(items) + + def remove(self, val): + """Removes the first argument with the given value from the OSCMessage. + Raises ValueError if val isn't found. + """ + items = list(self.items()) + + # this is not very efficient... + i = 0 + for (t, v) in items: + if (v == val): + break + i += 1 + else: + raise ValueError("'%s' not in OSCMessage" % str(m)) + # but more efficient than first calling self.values().index(val), + # then calling self.items(), which would in turn call self.values() again... + + del items[i] + + self._reencode(items) + + def __iter__(self): + """Returns an iterator of the OSCMessage's arguments + """ + return iter(list(self.values())) + + def __reversed__(self): + """Returns a reverse iterator of the OSCMessage's arguments + """ + return reversed(list(self.values())) + + def itervalues(self): + """Returns an iterator of the OSCMessage's arguments + """ + return iter(list(self.values())) + + def iteritems(self): + """Returns an iterator of the OSCMessage's arguments as + (typetag, value) tuples + """ + return iter(list(self.items())) + + def itertags(self): + """Returns an iterator of the OSCMessage's arguments' typetags + """ + return iter(self.tags()) + +class OSCBundle(OSCMessage): + """Builds a 'bundle' of OSC messages. + + OSCBundle objects are container objects for building OSC-bundles of OSC-messages. + An OSC-bundle is a special kind of OSC-message which contains a list of OSC-messages + (And yes, OSC-bundles may contain other OSC-bundles...) + + OSCBundle objects behave much the same as OSCMessage objects, with these exceptions: + - if an item or items to be appended or inserted are not OSCMessage objects, + OSCMessage objectss are created to encapsulate the item(s) + - an OSC-bundle does not have an address of its own, only the contained OSC-messages do. + The OSCBundle's 'address' is inherited by any OSCMessage the OSCBundle object creates. + - OSC-bundles have a timetag to tell the receiver when the bundle should be processed. + The default timetag value (0) means 'immediately' + """ + def __init__(self, address="", time=0): + """Instantiate a new OSCBundle. + The default OSC-address for newly created OSCMessages + can be specified with the 'address' argument + The bundle's timetag can be set with the 'time' argument + """ + super(OSCBundle, self).__init__(address) + self.timetag = time + + def __str__(self): + """Returns the Bundle's contents (and timetag, if nonzero) as a string. + """ + if (self.timetag > 0.): + out = "#bundle (%s) [" % self.getTimeTagStr() + else: + out = "#bundle [" + + if self.__len__(): + for val in list(self.values()): + out += "%s, " % str(val) + out = out[:-2] # strip trailing space and comma + + return out + "]" + + def setTimeTag(self, time): + """Set or change the OSCBundle's TimeTag + In 'Python Time', that's floating seconds since the Epoch + """ + if time >= 0: + self.timetag = time + + def getTimeTagStr(self): + """Return the TimeTag as a human-readable string + """ + fract, secs = math.modf(self.timetag) + out = time.ctime(secs)[11:19] + out += ("%.3f" % fract)[1:] + + return out + + def append(self, argument, typehint = None): + """Appends data to the bundle, creating an OSCMessage to encapsulate + the provided argument unless this is already an OSCMessage. + Any newly created OSCMessage inherits the OSCBundle's address at the time of creation. + If 'argument' is an iterable, its elements will be encapsuated by a single OSCMessage. + Finally, 'argument' can be (or contain) a dict, which will be 'converted' to an OSCMessage; + - if 'addr' appears in the dict, its value overrides the OSCBundle's address + - if 'args' appears in the dict, its value(s) become the OSCMessage's arguments + """ + if isinstance(argument, OSCMessage): + binary = OSCBlob(argument.getBinary()) + else: + msg = OSCMessage(self.address) + if isinstance(argument,dict): + if 'addr' in argument: + msg.setAddress(argument['addr']) + if 'args' in argument: + msg.append(argument['args'], typehint) + else: + msg.append(argument, typehint) + + binary = OSCBlob(msg.getBinary()) + + self.message += binary + self.typetags += 'b' + + def getBinary(self): + """Returns the binary representation of the message + """ + binary = OSCString("#bundle") + binary += OSCTimeTag(self.timetag) + binary += self.message + + return binary + + def _reencapsulate(self, decoded): + if decoded[0] == "#bundle": + msg = OSCBundle() + msg.setTimeTag(decoded[1]) + for submsg in decoded[2:]: + msg.append(self._reencapsulate(submsg)) + + else: + msg = OSCMessage(decoded[0]) + tags = decoded[1].lstrip(',') + for i in range(len(tags)): + msg.append(decoded[2+i], tags[i]) + + return msg + + def values(self): + """Returns a list of the OSCMessages appended so far + """ + out = [] + for decoded in decodeOSC(self.getBinary())[2:]: + out.append(self._reencapsulate(decoded)) + + return out + + def __eq__(self, other): + """Return True if two OSCBundles have the same timetag & content + """ + if not isinstance(other, self.__class__): + return False + + return (self.timetag == other.timetag) and (self.typetags == other.typetags) and (self.message == other.message) + + def copy(self): + """Returns a deep copy of this OSCBundle + """ + copy = super(OSCBundle, self).copy() + copy.timetag = self.timetag + return copy + +###### +# +# OSCMessage encoding functions +# +###### + +def OSCString(next): + """Convert a string into a zero-padded OSC String. + The length of the resulting string is always a multiple of 4 bytes. + The string ends with 1 to 4 zero-bytes ('\x00') + """ + + OSCstringLength = math.ceil((len(next)+1) / 4.0) * 4 + return struct.pack(">%ds" % (OSCstringLength), str(next).encode('latin1')) + +def OSCBlob(next): + """Convert a string into an OSC Blob. + An OSC-Blob is a binary encoded block of data, prepended by a 'size' (int32). + The size is always a mutiple of 4 bytes. + The blob ends with 0 to 3 zero-bytes ('\x00') + """ + + if isinstance(next,str): + next = next.encode('latin1') + if isinstance(next,bytes): + OSCblobLength = math.ceil((len(next)) / 4.0) * 4 + binary = struct.pack(">i%ds" % (OSCblobLength), OSCblobLength, next) + else: + binary = b'' + + return binary + +def OSCArgument(next, typehint=None): + """ Convert some Python types to their + OSC binary representations, returning a + (typetag, data) tuple. + """ + if not typehint: + if type(next) in FloatTypes: + binary = struct.pack(">f", float(next)) + tag = 'f' + elif type(next) in IntTypes: + binary = struct.pack(">i", int(next)) + tag = 'i' + else: + binary = OSCString(next) + tag = 's' + + elif typehint == 'd': + try: + binary = struct.pack(">d", float(next)) + tag = 'd' + except ValueError: + binary = OSCString(next) + tag = 's' + + elif typehint == 'f': + try: + binary = struct.pack(">f", float(next)) + tag = 'f' + except ValueError: + binary = OSCString(next) + tag = 's' + elif typehint == 'i': + try: + binary = struct.pack(">i", int(next)) + tag = 'i' + except ValueError: + binary = OSCString(next) + tag = 's' + else: + binary = OSCString(next) + tag = 's' + + return (tag, binary) + +def OSCTimeTag(time): + """Convert a time in floating seconds to its + OSC binary representation + """ + if time > 0: + fract, secs = math.modf(time) + secs = secs - NTP_epoch + binary = struct.pack('>LL', int(secs), int(fract * NTP_units_per_second)) + else: + binary = struct.pack('>LL', 0, 1) + + return binary + +###### +# +# OSCMessage decoding functions +# +###### + +def _readString(data): + """Reads the next (null-terminated) block of data + """ + length = data.find(b'\0') + nextData = int(math.ceil((length+1) / 4.0) * 4) + return (data[0:length].decode('latin1'), data[nextData:]) + +def _readBlob(data): + """Reads the next (numbered) block of data + """ + + length = struct.unpack(">i", data[0:4])[0] + nextData = int(math.ceil((length) / 4.0) * 4) + 4 + return (data[4:length+4], data[nextData:]) + +def _readInt(data): + """Tries to interpret the next 4 bytes of the data + as a 32-bit integer. """ + + if(len(data)<4): + print(("Error: too few bytes for int", data, len(data))) + rest = data + integer = 0 + else: + integer = struct.unpack(">i", data[0:4])[0] + rest = data[4:] + + return (integer, rest) + +def _readLong(data): + """Tries to interpret the next 8 bytes of the data + as a 64-bit signed integer. + """ + + high, low = struct.unpack(">ll", data[0:8]) + big = (int(high) << 32) + low + rest = data[8:] + return (big, rest) + +def _readTimeTag(data): + """Tries to interpret the next 8 bytes of the data + as a TimeTag. + """ + high, low = struct.unpack(">LL", data[0:8]) + if (high == 0) and (low <= 1): + time = 0.0 + else: + time = int(NTP_epoch + high) + float(low / NTP_units_per_second) + rest = data[8:] + return (time, rest) + +def _readFloat(data): + """Tries to interpret the next 4 bytes of the data + as a 32-bit float. + """ + + if(len(data)<4): + print(("Error: too few bytes for float", data, len(data))) + rest = data + float = 0 + else: + float = struct.unpack(">f", data[0:4])[0] + rest = data[4:] + + return (float, rest) + +def _readDouble(data): + """Tries to interpret the next 8 bytes of the data + as a 64-bit float. + """ + + if(len(data)<8): + print(("Error: too few bytes for double", data, len(data))) + rest = data + float = 0 + else: + float = struct.unpack(">d", data[0:8])[0] + rest = data[8:] + + return (float, rest) + +def decodeOSC(data): + """Converts a binary OSC message to a Python list. + """ + table = {"i":_readInt, "f":_readFloat, "s":_readString, "b":_readBlob, "d":_readDouble, "t":_readTimeTag} + decoded = [] + address, rest = _readString(data) + if address.startswith(","): + typetags = address + address = "" + else: + typetags = "" + + if address == "#bundle": + time, rest = _readTimeTag(rest) + decoded.append(address) + decoded.append(time) + while len(rest)>0: + length, rest = _readInt(rest) + decoded.append(decodeOSC(rest[:length])) + rest = rest[length:] + + elif len(rest)>0: + if not len(typetags): + typetags, rest = _readString(rest) + decoded.append(address) + decoded.append(typetags) + if typetags.startswith(","): + for tag in typetags[1:]: + value, rest = table[tag](rest) + decoded.append(value) + else: + raise OSCError("OSCMessage's typetag-string lacks the magic ','") + + return decoded + +###### +# +# Utility functions +# +###### + +def hexDump(bytes): + """ Useful utility; prints the string in hexadecimal. + """ + print("byte 0 1 2 3 4 5 6 7 8 9 A B C D E F") + + if isinstance(bytes,str): + bytes = bytes.encode('latin1') + num = len(bytes) + for i in range(num): + if (i) % 16 == 0: + line = "%02X0 : " % (i/16) + line += "%02X " % bytes[i] + if (i+1) % 16 == 0: + print(("%s: %s" % (line, repr(bytes[i-15:i+1])))) + line = "" + + bytes_left = num % 16 + if bytes_left: + print(("%s: %s" % (line.ljust(54), repr(bytes[-bytes_left:])))) + +def getUrlStr(*args): + """Convert provided arguments to a string in 'host:port/prefix' format + Args can be: + - (host, port) + - (host, port), prefix + - host, port + - host, port, prefix + """ + if not len(args): + return "" + + if type(args[0]) == tuple: + host = args[0][0] + port = args[0][1] + args = args[1:] + else: + host = args[0] + port = args[1] + args = args[2:] + + if len(args): + prefix = args[0] + else: + prefix = "" + + if len(host) and (host != '0.0.0.0'): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + else: + host = 'localhost' + + if isinstance(port,int): + return "%s:%d%s" % (host, port, prefix) + else: + return host + prefix + +def parseUrlStr(url): + """Convert provided string in 'host:port/prefix' format to it's components + Returns ((host, port), prefix) + """ + if not (isinstance(url,str) and len(url)): + return (None, '') + + i = url.find("://") + if i > -1: + url = url[i+3:] + + i = url.find(':') + if i > -1: + host = url[:i].strip() + tail = url[i+1:].strip() + else: + host = '' + tail = url + + for i in range(len(tail)): + if not tail[i].isdigit(): + break + else: + i += 1 + + portstr = tail[:i].strip() + tail = tail[i:].strip() + + found = len(tail) + for c in ('/', '+', '-', '*'): + i = tail.find(c) + if (i > -1) and (i < found): + found = i + + head = tail[:found].strip() + prefix = tail[found:].strip() + + prefix = prefix.strip('/') + if len(prefix) and prefix[0] not in ('+', '-', '*'): + prefix = '/' + prefix + + if len(head) and not len(host): + host = head + + if len(host): + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + try: + port = int(portstr) + except ValueError: + port = None + + return ((host, port), prefix) + +###### +# +# OSCClient class +# +###### + +class OSCClient(object): + """Simple OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket + """ + # set outgoing socket buffer size + sndbuf_size = 4096 * 8 + + def __init__(self, server=None): + """Construct an OSC Client. + When the 'address' argument is given this client is connected to a specific remote server. + - address ((host, port) tuple): the address of the remote server to send all messages to + Otherwise it acts as a generic client: + If address == 'None', the client doesn't connect to a specific remote server, + and the remote address must be supplied when calling sendto() + - server: Local OSCServer-instance this client will use the socket of for transmissions. + If none is supplied, a socket will be created. + """ + self.socket = None + + if server == None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self._fd = self.socket.fileno() + + self.server = None + else: + self.setServer(server) + + self.client_address = None + + def setServer(self, server): + """Associate this Client with given server. + The Client will send from the Server's socket. + The Server will use this Client instance to send replies. + """ + if not isinstance(server, OSCServer): + raise ValueError("'server' argument is not a valid OSCServer object") + + if self.socket != None: + self.close() + + self.socket = server.socket.dup() + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self._fd = self.socket.fileno() + + self.server = server + + if self.server.client != None: + self.server.client.close() + + self.server.client = self + + def close(self): + """Disconnect & close the Client's socket + """ + if self.socket != None: + self.socket.close() + self.socket = None + + def __str__(self): + """Returns a string containing this Client's Class-name, software-version + and the remote-address it is connected to (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.address() + if addr: + out += " connected to osc://%s" % getUrlStr(addr) + else: + out += " (unconnected)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + isequal = cmp(self.socket._sock, other.socket._sock) + if isequal and self.server and other.server: + return cmp(self.server, other.server) + + return isequal + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) + + def address(self): + """Returns a (host,port) tuple of the remote server this client is + connected to or None if not connected to any server. + """ + try: + return self.socket.getpeername() + except socket.error: + return None + + def connect(self, address): + """Bind to a specific OSC server: + the 'address' argument is a (host, port) tuple + - host: hostname of the remote OSC server, + - port: UDP-port the remote OSC server listens to. + """ + try: + self.socket.connect(address) + self.client_address = address + except socket.error as e: + self.client_address = None + raise OSCClientError("SocketError: %s" % str(e)) + + if self.server != None: + self.server.return_port = address[1] + + def sendto(self, msg, address, timeout=None): + """Send the given OSCMessage to the specified address. + - msg: OSCMessage (or OSCBundle) to be sent + - address: (host, port) tuple specifing remote server to send the message to + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + self.socket.connect(address) + self.socket.sendall(msg.getBinary()) + + if self.client_address: + self.socket.connect(self.client_address) + + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) + + def send(self, msg, timeout=None): + """Send the given OSCMessage. + The Client must be already connected. + - msg: OSCMessage (or OSCBundle) to be sent + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket, + or when the Client isn't connected to a remote server. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + self.socket.sendall(msg.getBinary()) + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending: %s" % str(e)) + +###### +# +# FilterString Utility functions +# +###### + +def parseFilterStr(args): + """Convert Message-Filter settings in '+ - ...' format to a dict of the form + { '':True, '':False, ... } + Returns a list: ['', filters] + """ + out = {} + + if isinstance(args,str): + args = [args] + + prefix = None + for arg in args: + head = None + for plus in arg.split('+'): + minus = plus.split('-') + plusfs = minus.pop(0).strip() + if len(plusfs): + plusfs = '/' + plusfs.strip('/') + + if (head == None) and (plusfs != "/*"): + head = plusfs + elif len(plusfs): + if plusfs == '/*': + out = { '/*':True } # reset all previous filters + else: + out[plusfs] = True + + for minusfs in minus: + minusfs = minusfs.strip() + if len(minusfs): + minusfs = '/' + minusfs.strip('/') + if minusfs == '/*': + out = { '/*':False } # reset all previous filters + else: + out[minusfs] = False + + if prefix == None: + prefix = head + + return [prefix, out] + +def getFilterStr(filters): + """Return the given 'filters' dict as a list of + '+' | '-' filter-strings + """ + if not len(filters): + return [] + + if '/*' in list(filters.keys()): + if filters['/*']: + out = ["+/*"] + else: + out = ["-/*"] + else: + if False in list(filters.values()): + out = ["+/*"] + else: + out = ["-/*"] + + for (addr, bool) in list(filters.items()): + if addr == '/*': + continue + + if bool: + out.append("+%s" % addr) + else: + out.append("-%s" % addr) + + return out + +# A translation-table for mapping OSC-address expressions to Python 're' expressions +OSCtrans = str.maketrans("{,}?","(|).") + +def getRegEx(pattern): + """Compiles and returns a 'regular expression' object for the given address-pattern. + """ + # Translate OSC-address syntax to python 're' syntax + pattern = pattern.replace(".", r"\.") # first, escape all '.'s in the pattern. + pattern = pattern.replace("(", r"\(") # escape all '('s. + pattern = pattern.replace(")", r"\)") # escape all ')'s. + pattern = pattern.replace("*", r".*") # replace a '*' by '.*' (match 0 or more characters) + pattern = pattern.translate(OSCtrans) # change '?' to '.' and '{,}' to '(|)' + + return re.compile(pattern) + +###### +# +# OSCMultiClient class +# +###### + +class OSCMultiClient(OSCClient): + """'Multiple-Unicast' OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket + This client keeps a dict of 'OSCTargets'. and sends each OSCMessage to each OSCTarget + The OSCTargets are simply (host, port) tuples, and may be associated with an OSC-address prefix. + the OSCTarget's prefix gets prepended to each OSCMessage sent to that target. + """ + def __init__(self, server=None): + """Construct a "Multi" OSC Client. + - server: Local OSCServer-instance this client will use the socket of for transmissions. + If none is supplied, a socket will be created. + """ + super(OSCMultiClient, self).__init__(server) + + self.targets = {} + + def _searchHostAddr(self, host): + """Search the subscribed OSCTargets for (the first occurence of) given host. + Returns a (host, port) tuple + """ + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + for addr in list(self.targets.keys()): + if host == addr[0]: + return addr + + raise NotSubscribedError((host, None)) + + def _updateFilters(self, dst, src): + """Update a 'filters' dict with values form another 'filters' dict: + - src[a] == True and dst[a] == False: del dst[a] + - src[a] == False and dst[a] == True: del dst[a] + - a not in dst: dst[a] == src[a] + """ + if '/*' in list(src.keys()): # reset filters + dst.clear() # 'match everything' == no filters + if not src.pop('/*'): + dst['/*'] = False # 'match nothing' + + for (addr, bool) in list(src.items()): + if (addr in list(dst.keys())) and (dst[addr] != bool): + del dst[addr] + else: + dst[addr] = bool + + def _setTarget(self, address, prefix=None, filters=None): + """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. + - address ((host, port) tuple): IP-address & UDP-port + - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage + sent to this OSCTarget (optional) + """ + if address not in list(self.targets.keys()): + self.targets[address] = ["",{}] + + if prefix != None: + if len(prefix): + # make sure prefix starts with ONE '/', and does not end with '/' + prefix = '/' + prefix.strip('/') + + self.targets[address][0] = prefix + + if filters != None: + if isinstance(filters,str): + (_, filters) = parseFilterStr(filters) + elif not isinstance(filters,dict): + raise TypeError("'filters' argument must be a dict with {addr:bool} entries") + + self._updateFilters(self.targets[address][1], filters) + + def setOSCTarget(self, address, prefix=None, filters=None): + """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. + the 'address' argument can be a ((host, port) tuple) : The target server address & UDP-port + or a 'host' (string) : The host will be looked-up + - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage + sent to this OSCTarget (optional) + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + elif (isinstance(address,tuple)): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except: + pass + + address = (host, port) + else: + raise TypeError("'address' argument must be a (host, port) tuple or a 'host' string") + + self._setTarget(address, prefix, filters) + + def setOSCTargetFromStr(self, url): + """Adds or modifies a subscribed OSCTarget from the given string, which should be in the + ':[/] [+/]|[-/] ...' format. + """ + (addr, tail) = parseUrlStr(url) + (prefix, filters) = parseFilterStr(tail) + self._setTarget(addr, prefix, filters) + + def _delTarget(self, address, prefix=None): + """Delete the specified OSCTarget from the Client's dict. + the 'address' argument must be a (host, port) tuple. + If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. + """ + try: + if prefix == None: + del self.targets[address] + elif prefix == self.targets[address][0]: + del self.targets[address] + except KeyError: + raise NotSubscribedError(address, prefix) + + def delOSCTarget(self, address, prefix=None): + """Delete the specified OSCTarget from the Client's dict. + the 'address' argument can be a ((host, port) tuple), or a hostname. + If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if isinstance(address,tuple): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + self._delTarget(address, prefix) + + def hasOSCTarget(self, address, prefix=None): + """Return True if the given OSCTarget exists in the Client's dict. + the 'address' argument can be a ((host, port) tuple), or a hostname. + If the 'prefix' argument is given, the return-value is only True if the address and prefix match. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if isinstance(address,tuple): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + if address in list(self.targets.keys()): + if prefix == None: + return True + elif prefix == self.targets[address][0]: + return True + + return False + + def getOSCTargets(self): + """Returns the dict of OSCTargets: {addr:[prefix, filters], ...} + """ + out = {} + for ((host, port), pf) in list(self.targets.items()): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + + out[(host, port)] = pf + + return out + + def getOSCTarget(self, address): + """Returns the OSCTarget matching the given address as a ((host, port), [prefix, filters]) tuple. + 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned + Returns (None, ['',{}]) if address not found. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if (isinstance(address,tuple)): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + if (address in list(self.targets.keys())): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + + return ((host, port), self.targets[address]) + + return (None, ['',{}]) + + def clearOSCTargets(self): + """Erases all OSCTargets from the Client's dict + """ + self.targets = {} + + def updateOSCTargets(self, dict): + """Update the Client's OSCTargets dict with the contents of 'dict' + The given dict's items MUST be of the form + { (host, port):[prefix, filters], ... } + """ + for ((host, port), (prefix, filters)) in list(dict.items()): + val = [prefix, {}] + self._updateFilters(val[1], filters) + + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + self.targets[(host, port)] = val + + def getOSCTargetStr(self, address): + """Returns the OSCTarget matching the given address as a ('osc://:[]', ['', ...])' tuple. + 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned + Returns (None, []) if address not found. + """ + (addr, (prefix, filters)) = self.getOSCTarget(address) + if addr == None: + return (None, []) + + return ("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters)) + + def getOSCTargetStrings(self): + """Returns a list of all OSCTargets as ('osc://:[]', ['', ...])' tuples. + """ + out = [] + for (addr, (prefix, filters)) in list(self.targets.items()): + out.append(("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters))) + + return out + + def connect(self, address): + """The OSCMultiClient isn't allowed to connect to any specific + address. + """ + return NotImplemented + + def sendto(self, msg, address, timeout=None): + """Send the given OSCMessage. + The specified address is ignored. Instead this method calls send() to + send the message to all subscribed clients. + - msg: OSCMessage (or OSCBundle) to be sent + - address: (host, port) tuple specifing remote server to send the message to + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + self.send(msg, timeout) + + def _filterMessage(self, filters, msg): + """Checks the given OSCMessge against the given filters. + 'filters' is a dict containing OSC-address:bool pairs. + If 'msg' is an OSCBundle, recursively filters its constituents. + Returns None if the message is to be filtered, else returns the message. + or + Returns a copy of the OSCBundle with the filtered messages removed. + """ + if isinstance(msg, OSCBundle): + out = msg.copy() + msgs = list(out.values()) + out.clearData() + for m in msgs: + m = self._filterMessage(filters, m) + if m: # this catches 'None' and empty bundles. + out.append(m) + + elif isinstance(msg, OSCMessage): + if '/*' in list(filters.keys()): + if filters['/*']: + out = msg + else: + out = None + + elif False in list(filters.values()): + out = msg + else: + out = None + + else: + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + expr = getRegEx(msg.address) + + for addr in list(filters.keys()): + if addr == '/*': + continue + + match = expr.match(addr) + if match and (match.end() == len(addr)): + if filters[addr]: + out = msg + else: + out = None + break + + return out + + def _prefixAddress(self, prefix, msg): + """Makes a copy of the given OSCMessage, then prepends the given prefix to + The message's OSC-address. + If 'msg' is an OSCBundle, recursively prepends the prefix to its constituents. + """ + out = msg.copy() + + if isinstance(msg, OSCBundle): + msgs = list(out.values()) + out.clearData() + for m in msgs: + out.append(self._prefixAddress(prefix, m)) + + elif isinstance(msg, OSCMessage): + out.setAddress(prefix + out.address) + + else: + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + return out + + def send(self, msg, timeout=None): + """Send the given OSCMessage to all subscribed OSCTargets + - msg: OSCMessage (or OSCBundle) to be sent + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + for (address, (prefix, filters)) in list(self.targets.items()): + if len(filters): + out = self._filterMessage(filters, msg) + if not out: # this catches 'None' and empty bundles. + continue + else: + out = msg + + if len(prefix): + out = self._prefixAddress(prefix, msg) + + binary = out.getBinary() + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + while len(binary): + sent = self.socket.sendto(binary, address) + binary = binary[sent:] + + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) + +class OSCAddressSpace: + def __init__(self): + self.callbacks = {} + def addMsgHandler(self, address, callback): + """Register a handler for an OSC-address + - 'address' is the OSC address-string. + the address-string should start with '/' and may not contain '*' + - 'callback' is the function called for incoming OSCMessages that match 'address'. + The callback-function will be called with the same arguments as the 'msgPrinter_handler' below + """ + for chk in '*?,[]{}# ': + if chk in address: + raise OSCServerError("OSC-address string may not contain any characters in '*?,[]{}# '") + + if type(callback) not in (types.FunctionType, types.MethodType): + raise OSCServerError("Message callback '%s' is not callable" % repr(callback)) + + if address != 'default': + address = '/' + address.strip('/') + + self.callbacks[address] = callback + + def delMsgHandler(self, address): + """Remove the registered handler for the given OSC-address + """ + del self.callbacks[address] + + def getOSCAddressSpace(self): + """Returns a list containing all OSC-addresses registerd with this Server. + """ + return list(self.callbacks.keys()) + + def dispatchMessage(self, pattern, tags, data, client_address): + """Attmept to match the given OSC-address pattern, which may contain '*', + against all callbacks registered with the OSCServer. + Calls the matching callback and returns whatever it returns. + If no match is found, and a 'default' callback is registered, it calls that one, + or raises NoCallbackError if a 'default' callback is not registered. + + - pattern (string): The OSC-address of the receied message + - tags (string): The OSC-typetags of the receied message's arguments, without ',' + - data (list): The message arguments + """ + if len(tags) != len(data): + raise OSCServerError("Malformed OSC-message; got %d typetags [%s] vs. %d values" % (len(tags), tags, len(data))) + + expr = getRegEx(pattern) + + replies = [] + matched = 0 + for addr in list(self.callbacks.keys()): + match = expr.match(addr) + if match and (match.end() == len(addr)): + reply = self.callbacks[addr](pattern, tags, data, client_address) + matched += 1 + if isinstance(reply, OSCMessage): + replies.append(reply) + elif reply != None: + raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks[addr], type(reply))) + + if matched == 0: + if 'default' in self.callbacks: + reply = self.callbacks['default'](pattern, tags, data, client_address) + if isinstance(reply, OSCMessage): + replies.append(reply) + elif reply != None: + raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks['default'], type(reply))) + else: + raise NoCallbackError(pattern) + + return replies + +###### +# +# OSCRequestHandler classes +# +###### +class OSCRequestHandler(DatagramRequestHandler): + """RequestHandler class for the OSCServer + """ + def setup(self): + """Prepare RequestHandler. + Unpacks request as (packet, source socket address) + Creates an empty list for replies. + """ + (self.packet, self.socket) = self.request + self.replies = [] + + def _unbundle(self, decoded): + """Recursive bundle-unpacking function""" + if decoded[0] != "#bundle": + self.replies += self.server.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def handle(self): + """Handle incoming OSCMessage + """ + decoded = decodeOSC(self.packet) + if not len(decoded): + return + + self._unbundle(decoded) + + def finish(self): + """Finish handling OSCMessage. + Send any reply returned by the callback(s) back to the originating client + as an OSCMessage or OSCBundle + """ + if self.server.return_port: + self.client_address = (self.client_address[0], self.server.return_port) + + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + return + + self.server.client.sendto(msg, self.client_address) + +class ThreadingOSCRequestHandler(OSCRequestHandler): + """Multi-threaded OSCRequestHandler; + Starts a new RequestHandler thread for each unbundled OSCMessage + """ + def _unbundle(self, decoded): + """Recursive bundle-unpacking function + This version starts a new thread for each sub-Bundle found in the Bundle, + then waits for all its children to finish. + """ + if decoded[0] != "#bundle": + self.replies += self.server.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + now = time.time() + + children = [] + + for msg in decoded[2:]: + t = threading.Thread(target = self._unbundle, args = (msg,)) + t.start() + children.append(t) + + # wait for all children to terminate + for t in children: + t.join() + +###### +# +# OSCServer classes +# +###### +class OSCServer(UDPServer, OSCAddressSpace): + """A Synchronous OSCServer + Serves one request at-a-time, until the OSCServer is closed. + The OSC address-pattern is matched against a set of OSC-adresses + that have been registered to the server with a callback-function. + If the adress-pattern of the message machtes the registered address of a callback, + that function is called. + """ + + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = OSCRequestHandler + + # define a socket timeout, so the serve_forever loop can actually exit. + socket_timeout = 1 + + # DEBUG: print error-tracebacks (to stderr)? + print_tracebacks = False + + def __init__(self, server_address, client=None, return_port=0): + """Instantiate an OSCServer. + - server_address ((host, port) tuple): the local host & UDP-port + the server listens on + - client (OSCClient instance): The OSCClient used to send replies from this server. + If none is supplied (default) an OSCClient will be created. + - return_port (int): if supplied, sets the default UDP destination-port + for replies coming from this server. + """ + UDPServer.__init__(self, server_address, self.RequestHandlerClass) + OSCAddressSpace.__init__(self) + + self.setReturnPort(return_port) + self.error_prefix = "" + self.info_prefix = "/info" + + self.socket.settimeout(self.socket_timeout) + + self.running = False + self.client = None + + if client == None: + self.client = OSCClient(server=self) + else: + self.setClient(client) + + def setClient(self, client): + """Associate this Server with a new local Client instance, closing the Client this Server is currently using. + """ + if not isinstance(client, OSCClient): + raise ValueError("'client' argument is not a valid OSCClient object") + + if client.server != None: + raise OSCServerError("Provided OSCClient already has an OSCServer-instance: %s" % str(client.server)) + + # Server socket is already listening at this point, so we can't use the client's socket. + # we'll have to force our socket on the client... + client_address = client.address() # client may be already connected + client.close() # shut-down that socket + + # force our socket upon the client + client.socket = self.socket.dup() + client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, client.sndbuf_size) + client._fd = client.socket.fileno() + client.server = self + + if client_address: + client.connect(client_address) + if not self.return_port: + self.return_port = client_address[1] + + if self.client != None: + self.client.close() + + self.client = client + + def serve_forever(self): + """Handle one request at a time until server is closed.""" + self.running = True + while self.running: + self.handle_request() # this times-out when no data arrives. + + def close(self): + """Stops serving requests, closes server (socket), closes used client + """ + self.running = False + self.client.close() + self.server_close() + + def __str__(self): + """Returns a string containing this Server's Class-name, software-version and local bound address (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.address() + if addr: + out += " listening on osc://%s" % getUrlStr(addr) + else: + out += " (unbound)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + return cmp(self.socket._sock, other.socket._sock) + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) + + def address(self): + """Returns a (host,port) tuple of the local address this server is bound to, + or None if not bound to any address. + """ + try: + return self.socket.getsockname() + except socket.error: + return None + + def setReturnPort(self, port): + """Set the destination UDP-port for replies returning from this server to the remote client + """ + if (port > 1024) and (port < 65536): + self.return_port = port + else: + self.return_port = None + + + def setSrvInfoPrefix(self, pattern): + """Set the first part of OSC-address (pattern) this server will use to reply to server-info requests. + """ + if len(pattern): + pattern = '/' + pattern.strip('/') + + self.info_prefix = pattern + + def setSrvErrorPrefix(self, pattern=""): + """Set the OSC-address (pattern) this server will use to report errors occuring during + received message handling to the remote client. + + If pattern is empty (default), server-errors are not reported back to the client. + """ + if len(pattern): + pattern = '/' + pattern.strip('/') + + self.error_prefix = pattern + + def addDefaultHandlers(self, prefix="", info_prefix="/info", error_prefix="/error"): + """Register a default set of OSC-address handlers with this Server: + - 'default' -> noCallback_handler + the given prefix is prepended to all other callbacks registered by this method: + - ' serverInfo_handler + - ' -> msgPrinter_handler + - '/print' -> msgPrinter_handler + and, if the used Client supports it; + - '/subscribe' -> subscription_handler + - '/unsubscribe' -> subscription_handler + + Note: the given 'error_prefix' argument is also set as default 'error_prefix' for error-messages + *sent from* this server. This is ok, because error-messages generally do not elicit a reply from the receiver. + + To do this with the serverInfo-prefixes would be a bad idea, because if a request received on '/info' (for example) + would send replies to '/info', this could potentially cause a never-ending loop of messages! + Do *not* set the 'info_prefix' here (for incoming serverinfo requests) to the same value as given to + the setSrvInfoPrefix() method (for *replies* to incoming serverinfo requests). + For example, use '/info' for incoming requests, and '/inforeply' or '/serverinfo' or even just '/print' as the + info-reply prefix. + """ + self.error_prefix = error_prefix + self.addMsgHandler('default', self.noCallback_handler) + self.addMsgHandler(prefix + info_prefix, self.serverInfo_handler) + self.addMsgHandler(prefix + error_prefix, self.msgPrinter_handler) + self.addMsgHandler(prefix + '/print', self.msgPrinter_handler) + + if isinstance(self.client, OSCMultiClient): + self.addMsgHandler(prefix + '/subscribe', self.subscription_handler) + self.addMsgHandler(prefix + '/unsubscribe', self.subscription_handler) + + def printErr(self, txt): + """Writes 'OSCServer: txt' to sys.stderr + """ + sys.stderr.write("OSCServer: %s\n" % txt) + + def sendOSCerror(self, txt, client_address): + """Sends 'txt', encapsulated in an OSCMessage to the default 'error_prefix' OSC-addres. + Message is sent to the given client_address, with the default 'return_port' overriding + the client_address' port, if defined. + """ + lines = txt.split('\n') + if len(lines) == 1: + msg = OSCMessage(self.error_prefix) + msg.append(lines[0]) + elif len(lines) > 1: + msg = OSCBundle(self.error_prefix) + for line in lines: + msg.append(line) + else: + return + + if self.return_port: + client_address = (client_address[0], self.return_port) + + self.client.sendto(msg, client_address) + + def reportErr(self, txt, client_address): + """Writes 'OSCServer: txt' to sys.stderr + If self.error_prefix is defined, sends 'txt' as an OSC error-message to the client(s) + (see printErr() and sendOSCerror()) + """ + self.printErr(txt) + + if len(self.error_prefix): + self.sendOSCerror(txt, client_address) + + def sendOSCinfo(self, txt, client_address): + """Sends 'txt', encapsulated in an OSCMessage to the default 'info_prefix' OSC-addres. + Message is sent to the given client_address, with the default 'return_port' overriding + the client_address' port, if defined. + """ + lines = txt.split('\n') + if len(lines) == 1: + msg = OSCMessage(self.info_prefix) + msg.append(lines[0]) + elif len(lines) > 1: + msg = OSCBundle(self.info_prefix) + for line in lines: + msg.append(line) + else: + return + + if self.return_port: + client_address = (client_address[0], self.return_port) + + self.client.sendto(msg, client_address) + + ### + # Message-Handler callback functions + ### + + def handle_error(self, request, client_address): + """Handle an exception in the Server's callbacks gracefully. + Writes the error to sys.stderr and, if the error_prefix (see setSrvErrorPrefix()) is set, + sends the error-message as reply to the client + """ + (e_type, e) = sys.exc_info()[:2] + self.printErr("%s on request from %s: %s" % (e_type.__name__, getUrlStr(client_address), str(e))) + + if self.print_tracebacks: + import traceback + traceback.print_exc() # XXX But this goes to stderr! + + if len(self.error_prefix): + self.sendOSCerror("%s: %s" % (e_type.__name__, str(e)), client_address) + + def noCallback_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler prints a "No callback registered to handle ..." message. + Returns None + """ + self.reportErr("No callback registered to handle OSC-address '%s'" % addr, client_address) + + def msgPrinter_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler prints the received message. + Returns None + """ + txt = "OSCMessage '%s' from %s: " % (addr, getUrlStr(client_address)) + txt += str(data) + + self.printErr(txt) # strip trailing comma & space + + def serverInfo_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler returns a reply to the client, which can contain various bits of information + about this server, depending on the first argument of the received OSC-message: + - 'help' | 'info' : Reply contains server type & version info, plus a list of + available 'commands' understood by this handler + - 'list' | 'ls' : Reply is a bundle of 'address ' messages, listing the server's + OSC address-space. + - 'clients' | 'targets' : Reply is a bundle of 'target osc://:[] [] [...]' + messages, listing the local Client-instance's subscribed remote clients. + """ + if len(data) == 0: + return None + + cmd = data.pop(0) + + reply = None + if cmd in ('help', 'info'): + reply = OSCBundle(self.info_prefix) + reply.append(('server', str(self))) + reply.append(('info_command', "ls | list : list OSC address-space")) + reply.append(('info_command', "clients | targets : list subscribed clients")) + elif cmd in ('ls', 'list'): + reply = OSCBundle(self.info_prefix) + for addr in list(self.callbacks.keys()): + reply.append(('address', addr)) + elif cmd in ('clients', 'targets'): + if hasattr(self.client, 'getOSCTargetStrings'): + reply = OSCBundle(self.info_prefix) + for trg in self.client.getOSCTargetStrings(): + reply.append(('target',) + trg) + else: + cli_addr = self.client.address() + if cli_addr: + reply = OSCMessage(self.info_prefix) + reply.append(('target', "osc://%s/" % getUrlStr(cli_addr))) + else: + self.reportErr("unrecognized command '%s' in /info request from osc://%s. Try 'help'" % (cmd, getUrlStr(client_address)), client_address) + + return reply + + def _subscribe(self, data, client_address): + """Handle the actual subscription. the provided 'data' is concatenated together to form a + ':[] [] [...]' string, which is then passed to + parseUrlStr() & parseFilterStr() to actually retreive , , etc. + + This 'long way 'round' approach (almost) guarantees that the subscription works, + regardless of how the bits of the are encoded in 'data'. + """ + url = "" + have_port = False + for item in data: + if (isinstance(item,int)) and not have_port: + url += ":%d" % item + have_port = True + elif isinstance(item,str): + url += item + + (addr, tail) = parseUrlStr(url) + (prefix, filters) = parseFilterStr(tail) + + if addr != None: + (host, port) = addr + if not host: + host = client_address[0] + if not port: + port = client_address[1] + addr = (host, port) + else: + addr = client_address + + self.client._setTarget(addr, prefix, filters) + + trg = self.client.getOSCTargetStr(addr) + if trg[0] != None: + reply = OSCMessage(self.info_prefix) + reply.append(('target',) + trg) + return reply + + def _unsubscribe(self, data, client_address): + """Handle the actual unsubscription. the provided 'data' is concatenated together to form a + ':[]' string, which is then passed to + parseUrlStr() to actually retreive , & . + + This 'long way 'round' approach (almost) guarantees that the unsubscription works, + regardless of how the bits of the are encoded in 'data'. + """ + url = "" + have_port = False + for item in data: + if (isinstance(item,int)) and not have_port: + url += ":%d" % item + have_port = True + elif isinstance(item,str): + url += item + + (addr, _) = parseUrlStr(url) + + if addr == None: + addr = client_address + else: + (host, port) = addr + if not host: + host = client_address[0] + if not port: + try: + (host, port) = self.client._searchHostAddr(host) + except NotSubscribedError: + port = client_address[1] + + addr = (host, port) + + try: + self.client._delTarget(addr) + except NotSubscribedError as e: + txt = "%s: %s" % (e.__class__.__name__, str(e)) + self.printErr(txt) + + reply = OSCMessage(self.error_prefix) + reply.append(txt) + return reply + + def subscription_handler(self, addr, tags, data, client_address): + """Handle 'subscribe' / 'unsubscribe' requests from remote hosts, + if the local Client supports this (i.e. OSCMultiClient). + + Supported commands: + - 'help' | 'info' : Reply contains server type & version info, plus a list of + available 'commands' understood by this handler + - 'list' | 'ls' : Reply is a bundle of 'target osc://:[] [] [...]' + messages, listing the local Client-instance's subscribed remote clients. + - '[subscribe | listen | sendto | target] [ ...] : Subscribe remote client/server at , + and/or set message-filters for messages being sent to the subscribed host, with the optional + arguments. Filters are given as OSC-addresses (or '*') prefixed by a '+' (send matching messages) or + a '-' (don't send matching messages). The wildcard '*', '+*' or '+/*' means 'send all' / 'filter none', + and '-*' or '-/*' means 'send none' / 'filter all' (which is not the same as unsubscribing!) + Reply is an OSCMessage with the (new) subscription; 'target osc://:[] [] [...]' + - '[unsubscribe | silence | nosend | deltarget] : Unsubscribe remote client/server at + If the given isn't subscribed, a NotSubscribedError-message is printed (and possibly sent) + + The given to the subscribe/unsubscribe handler should be of the form: + '[osc://][][:][]', where any or all components can be omitted. + + If is not specified, the IP-address of the message's source is used. + If is not specified, the is first looked up in the list of subscribed hosts, and if found, + the associated port is used. + If is not specified and is not yet subscribed, the message's source-port is used. + If is specified on subscription, is prepended to the OSC-address of all messages + sent to the subscribed host. + If is specified on unsubscription, the subscribed host is only unsubscribed if the host, + port and prefix all match the subscription. + If is not specified on unsubscription, the subscribed host is unsubscribed if the host and port + match the subscription. + """ + if not isinstance(self.client, OSCMultiClient): + raise OSCServerError("Local %s does not support subsctiptions or message-filtering" % self.client.__class__.__name__) + + addr_cmd = addr.split('/')[-1] + + if len(data): + if data[0] in ('help', 'info'): + reply = OSCBundle(self.info_prefix) + reply.append(('server', str(self))) + reply.append(('subscribe_command', "ls | list : list subscribed targets")) + reply.append(('subscribe_command', "[subscribe | listen | sendto | target] [ ...] : subscribe to messages, set filters")) + reply.append(('subscribe_command', "[unsubscribe | silence | nosend | deltarget] : unsubscribe from messages")) + return reply + + if data[0] in ('ls', 'list'): + reply = OSCBundle(self.info_prefix) + for trg in self.client.getOSCTargetStrings(): + reply.append(('target',) + trg) + return reply + + if data[0] in ('subscribe', 'listen', 'sendto', 'target'): + return self._subscribe(data[1:], client_address) + + if data[0] in ('unsubscribe', 'silence', 'nosend', 'deltarget'): + return self._unsubscribe(data[1:], client_address) + + if addr_cmd in ('subscribe', 'listen', 'sendto', 'target'): + return self._subscribe(data, client_address) + + if addr_cmd in ('unsubscribe', 'silence', 'nosend', 'deltarget'): + return self._unsubscribe(data, client_address) + +class ForkingOSCServer(ForkingMixIn, OSCServer): + """An Asynchronous OSCServer. + This server forks a new process to handle each incoming request. + """ + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = ThreadingOSCRequestHandler + +class ThreadingOSCServer(ThreadingMixIn, OSCServer): + """An Asynchronous OSCServer. + This server starts a new thread to handle each incoming request. + """ + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = ThreadingOSCRequestHandler + +###### +# +# OSCError classes +# +###### + +class OSCError(Exception): + """Base Class for all OSC-related errors + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + +class OSCClientError(OSCError): + """Class for all OSCClient errors + """ + pass + +class OSCServerError(OSCError): + """Class for all OSCServer errors + """ + pass + +class NoCallbackError(OSCServerError): + """This error is raised (by an OSCServer) when an OSCMessage with an 'unmatched' address-pattern + is received, and no 'default' handler is registered. + """ + def __init__(self, pattern): + """The specified 'pattern' should be the OSC-address of the 'unmatched' message causing the error to be raised. + """ + self.message = "No callback registered to handle OSC-address '%s'" % pattern + +class NotSubscribedError(OSCClientError): + """This error is raised (by an OSCMultiClient) when an attempt is made to unsubscribe a host + that isn't subscribed. + """ + def __init__(self, addr, prefix=None): + if prefix: + url = getUrlStr(addr, prefix) + else: + url = getUrlStr(addr, '') + + self.message = "Target osc://%s is not subscribed" % url + +###### +# +# OSC over streaming transport layers (usually TCP) +# +# Note from the OSC 1.0 specifications about streaming protocols: +# +# The underlying network that delivers an OSC packet is responsible for +# delivering both the contents and the size to the OSC application. An OSC +# packet can be naturally represented by a datagram by a network protocol such +# as UDP. In a stream-based protocol such as TCP, the stream should begin with +# an int32 giving the size of the first packet, followed by the contents of the +# first packet, followed by the size of the second packet, etc. +# +# The contents of an OSC packet must be either an OSC Message or an OSC Bundle. +# The first byte of the packet's contents unambiguously distinguishes between +# these two alternatives. +# +###### + +class OSCStreamRequestHandler(StreamRequestHandler, OSCAddressSpace): + """ This is the central class of a streaming OSC server. If a client + connects to the server, the server instantiates a OSCStreamRequestHandler + for each new connection. This is fundamentally different to a packet + oriented server which has a single address space for all connections. + This connection based (streaming) OSC server maintains an address space + for each single connection, because usually tcp server spawn a new thread + or process for each new connection. This would generate severe + multithreading synchronization problems when each thread would operate on + the same address space object. Therefore: To implement a streaming/TCP OSC + server a custom handler must be implemented which implements the + setupAddressSpace member in which it creates its own address space for this + very connection. This has been done within the testbench and can serve as + inspiration. + """ + def __init__(self, request, client_address, server): + """ Initialize all base classes. The address space must be initialized + before the stream request handler because the initialization function + of the stream request handler calls the setup member which again + requires an already initialized address space. + """ + self._txMutex = threading.Lock() + OSCAddressSpace.__init__(self) + StreamRequestHandler.__init__(self, request, client_address, server) + + def _unbundle(self, decoded): + """Recursive bundle-unpacking function""" + if decoded[0] != "#bundle": + self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def setup(self): + StreamRequestHandler.setup(self) + print("SERVER: New client connection.") + self.setupAddressSpace() + self.server._clientRegister(self) + + def setupAddressSpace(self): + """ Override this function to customize your address space. """ + pass + + def finish(self): + StreamRequestHandler.finish(self) + self.server._clientUnregister(self) + print("SERVER: Client connection handled.") + def _transmit(self, data): + sent = 0 + while sent < len(data): + tmp = self.connection.send(data[sent:]) + if tmp == 0: + return False + sent += tmp + return True + def _transmitMsg(self, msg): + """Send an OSC message over a streaming socket. Raises exception if it + should fail. If everything is transmitted properly, True is returned. If + socket has been closed, False. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + try: + binary = msg.getBinary() + length = len(binary) + # prepend length of packet before the actual message (big endian) + len_big_endian = array.array('c', '\0' * 4) + struct.pack_into(">L", len_big_endian, 0, length) + len_big_endian = len_big_endian.tostring() + if self._transmit(len_big_endian) and self._transmit(binary): + return True + return False + except socket.error as e: + if e[0] == errno.EPIPE: # broken pipe + return False + raise e + + def _receive(self, count): + """ Receive a certain amount of data from the socket and return it. If the + remote end should be closed in the meanwhile None is returned. + """ + chunk = self.connection.recv(count) + if not chunk or len(chunk) == 0: + return None + while len(chunk) < count: + tmp = self.connection.recv(count - len(chunk)) + if not tmp or len(tmp) == 0: + return None + chunk = chunk + tmp + return chunk + + def _receiveMsg(self): + """ Receive OSC message from a socket and decode. + If an error occurs, None is returned, else the message. + """ + # get OSC packet size from stream which is prepended each transmission + chunk = self._receive(4) + if chunk == None: + print("SERVER: Socket has been closed.") + return None + # extract message length from big endian unsigned long (32 bit) + slen = struct.unpack(">L", chunk)[0] + # receive the actual message + chunk = self._receive(slen) + if chunk == None: + print("SERVER: Socket has been closed.") + return None + # decode OSC data and dispatch + msg = decodeOSC(chunk) + if msg == None: + raise OSCError("SERVER: Message decoding failed.") + return msg + + def handle(self): + """ + Handle a connection. + """ + # set socket blocking to avoid "resource currently not available" + # exceptions, because the connection socket inherits the settings + # from the listening socket and this times out from time to time + # in order to provide a way to shut the server down. But we want + # clean and blocking behaviour here + self.connection.settimeout(None) + + print("SERVER: Entered server loop") + try: + while True: + decoded = self._receiveMsg() + if decoded == None: + return + elif len(decoded) <= 0: + # if message decoding fails we try to stay in sync but print a message + print("OSC stream server: Spurious message received.") + continue + + self.replies = [] + self._unbundle(decoded) + + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + # no replies, continue receiving + continue + self._txMutex.acquire() + txOk = self._transmitMsg(msg) + self._txMutex.release() + if not txOk: + break + + except socket.error as e: + if e[0] == errno.ECONNRESET: + # if connection has been reset by client, we do not care much + # about it, we just assume our duty fullfilled + print("SERVER: Connection has been reset by peer.") + else: + raise e + + def sendOSC(self, oscData): + """ This member can be used to transmit OSC messages or OSC bundles + over the client/server connection. It is thread save. + """ + self._txMutex.acquire() + result = self._transmitMsg(oscData) + self._txMutex.release() + return result + +""" TODO Note on threaded unbundling for streaming (connection oriented) +transport: + +Threaded unbundling as implemented in ThreadingOSCServer must be implemented in +a different way for the streaming variant, because contrary to the datagram +version the streaming handler is instantiated only once per connection. This +leads to the problem (if threaded unbundling is implemented as in OSCServer) +that all further message reception is blocked until all (previously received) +pending messages are processed. + +Each StreamRequestHandler should provide a so called processing queue in which +all pending messages or subbundles are inserted to be processed in the future). +When a subbundle or message gets queued, a mechanism must be provided that +those messages get invoked when time asks for them. There are the following +opportunities: + - a timer is started which checks at regular intervals for messages in the + queue (polling - requires CPU resources) + - a dedicated timer is started for each message (requires timer resources) +""" + +class OSCStreamingServer(TCPServer): + """ A connection oriented (TCP/IP) OSC server. + """ + + # define a socket timeout, so the serve_forever loop can actually exit. + # with 2.6 and server.shutdown this wouldn't be necessary + socket_timeout = 1 + + # this is the class which handles a new connection. Override this for a + # useful customized server. See the testbench for an example + RequestHandlerClass = OSCStreamRequestHandler + + def __init__(self, address): + """Instantiate an OSCStreamingServer. + - server_address ((host, port) tuple): the local host & UDP-port + the server listens for new connections. + """ + self._clientList = [] + self._clientListMutex = threading.Lock() + TCPServer.__init__(self, address, self.RequestHandlerClass) + self.socket.settimeout(self.socket_timeout) + + def serve_forever(self): + """Handle one request at a time until server is closed. + Had to add this since 2.5 does not support server.shutdown() + """ + self.running = True + while self.running: + self.handle_request() # this times-out when no data arrives. + + def start(self): + """ Start the server thread. """ + self._server_thread = threading.Thread(target=self.serve_forever) + self._server_thread.setDaemon(True) + self._server_thread.start() + + def stop(self): + """ Stop the server thread and close the socket. """ + self.running = False + self._server_thread.join() + self.server_close() + # 2.6 only + #self.shutdown() + + def _clientRegister(self, client): + """ Gets called by each request/connection handler when connection is + established to add itself to the client list + """ + self._clientListMutex.acquire() + self._clientList.append(client) + self._clientListMutex.release() + + def _clientUnregister(self, client): + """ Gets called by each request/connection handler when connection is + lost to remove itself from the client list + """ + self._clientListMutex.acquire() + self._clientList.remove(client) + self._clientListMutex.release() + + def broadcastToClients(self, oscData): + """ Send OSC message or bundle to all connected clients. """ + result = True + for client in self._clientList: + result = result and client.sendOSC(oscData) + return result + +class OSCStreamingServerThreading(ThreadingMixIn, OSCStreamingServer): + pass + """ Implements a server which spawns a separate thread for each incoming + connection. Care must be taken since the OSC address space is for all + the same. + """ + +class OSCStreamingClient(OSCAddressSpace): + """ OSC streaming client. + A streaming client establishes a connection to a streaming server but must + be able to handle replies by the server as well. To accomplish this the + receiving takes place in a secondary thread, because no one knows if we + have to expect a reply or not, i.e. synchronous architecture doesn't make + much sense. + Replies will be matched against the local address space. If message + handlers access code of the main thread (where the client messages are sent + to the server) care must be taken e.g. by installing sychronization + mechanisms or by using an event dispatcher which can handle events + originating from other threads. + """ + # set outgoing socket buffer size + sndbuf_size = 4096 * 8 + rcvbuf_size = 4096 * 8 + + def __init__(self): + self._txMutex = threading.Lock() + OSCAddressSpace.__init__(self) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, self.rcvbuf_size) + self.socket.settimeout(1.0) + self._running = False + + def _receiveWithTimeout(self, count): + chunk = str() + while len(chunk) < count: + try: + tmp = self.socket.recv(count - len(chunk)) + except socket.timeout: + if not self._running: + print("CLIENT: Socket timed out and termination requested.") + return None + else: + continue + except socket.error as e: + if e[0] == errno.ECONNRESET: + print("CLIENT: Connection reset by peer.") + return None + else: + raise e + if not tmp or len(tmp) == 0: + print("CLIENT: Socket has been closed.") + return None + chunk = chunk + tmp + return chunk + def _receiveMsgWithTimeout(self): + """ Receive OSC message from a socket and decode. + If an error occurs, None is returned, else the message. + """ + # get OSC packet size from stream which is prepended each transmission + chunk = self._receiveWithTimeout(4) + if not chunk: + return None + # extract message length from big endian unsigned long (32 bit) + slen = struct.unpack(">L", chunk)[0] + # receive the actual message + chunk = self._receiveWithTimeout(slen) + if not chunk: + return None + # decode OSC content + msg = decodeOSC(chunk) + if msg == None: + raise OSCError("CLIENT: Message decoding failed.") + return msg + + def _receiving_thread_entry(self): + print("CLIENT: Entered receiving thread.") + self._running = True + while self._running: + decoded = self._receiveMsgWithTimeout() + if not decoded: + break + elif len(decoded) <= 0: + continue + + self.replies = [] + self._unbundle(decoded) + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + continue + self._txMutex.acquire() + txOk = self._transmitMsgWithTimeout(msg) + self._txMutex.release() + if not txOk: + break + print("CLIENT: Receiving thread terminated.") + + def _unbundle(self, decoded): + if decoded[0] != "#bundle": + self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.socket.getpeername()) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def connect(self, address): + self.socket.connect(address) + self.receiving_thread = threading.Thread(target=self._receiving_thread_entry) + self.receiving_thread.start() + + def close(self): + # let socket time out + self._running = False + self.receiving_thread.join() + self.socket.close() + + def _transmitWithTimeout(self, data): + sent = 0 + while sent < len(data): + try: + tmp = self.socket.send(data[sent:]) + except socket.timeout: + if not self._running: + print("CLIENT: Socket timed out and termination requested.") + return False + else: + continue + except socket.error as e: + if e[0] == errno.ECONNRESET: + print("CLIENT: Connection reset by peer.") + return False + else: + raise e + if tmp == 0: + return False + sent += tmp + return True + + def _transmitMsgWithTimeout(self, msg): + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + binary = msg.getBinary() + length = len(binary) + # prepend length of packet before the actual message (big endian) + len_big_endian = array.array('c', '\0' * 4) + struct.pack_into(">L", len_big_endian, 0, length) + len_big_endian = len_big_endian.tostring() + if self._transmitWithTimeout(len_big_endian) and self._transmitWithTimeout(binary): + return True + else: + return False + + def sendOSC(self, msg): + """Send an OSC message or bundle to the server. Returns True on success. + """ + self._txMutex.acquire() + txOk = self._transmitMsgWithTimeout(msg) + self._txMutex.release() + return txOk + + def __str__(self): + """Returns a string containing this Client's Class-name, software-version + and the remote-address it is connected to (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.socket.getpeername() + if addr: + out += " connected to osc://%s" % getUrlStr(addr) + else: + out += " (unconnected)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + isequal = cmp(self.socket._sock, other.socket._sock) + if isequal and self.server and other.server: + return cmp(self.server, other.server) + + return isequal + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) diff --git a/generators/blank.py b/generators/blank.py new file mode 100755 index 0000000..657bc5a --- /dev/null +++ b/generators/blank.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +Send only black points +v0.1.0 + +Use it to test your filters and outputs + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import time +import argparse +import sys +name="generator::dummy" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="dummy generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + + +shape = [[400,400,0],[400,400,64],[400,400,0]] + + +while True: + start = time.time() + print(shape, flush=True); + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/generators/book2.ild b/generators/book2.ild new file mode 100755 index 0000000000000000000000000000000000000000..608803e131869c30b36433f50ff07d73582c2831 GIT binary patch literal 108656 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 CWy#S1 literal 0 HcmV?d00001 diff --git a/generators/brmlab1.svg b/generators/brmlab1.svg new file mode 100755 index 0000000..05bf423 --- /dev/null +++ b/generators/brmlab1.svg @@ -0,0 +1,54 @@ + +image/svg+xml + + \ No newline at end of file diff --git a/generators/brmlab2.svg b/generators/brmlab2.svg new file mode 100755 index 0000000..5c3a447 --- /dev/null +++ b/generators/brmlab2.svg @@ -0,0 +1,53 @@ + +image/svg+xml + \ No newline at end of file diff --git a/generators/dummy.py b/generators/dummy.py new file mode 100755 index 0000000..8f7eb2c --- /dev/null +++ b/generators/dummy.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +This is the most basic generator you can imagine: straight up static! +v0.1.0 + +Use it to test your filters and outputs + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import time +import argparse +import sys +name="generator::dummy" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="dummy generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-s","--speed",help="point per frame progress",default=3,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) +color = 16777215 +square = [[100.0, 100.0, color], [100.0, 500.0, color], [500.0, 500.0, color], [500.0, 100.0, color], [100.0, 100.0, color]] +line =[] +for i in range(00,800,int(800/120)): + line.append([i, 400, color]) +square = [[100.0, 100.0, color], [100.0, 500.0, color], [500.0, 500.0, color], [500.0, 100.0, color], [100.0, 100.0, color]] +mire = [ + [600,600,0], + [600,600,color], + [700,600,color], + [700,700,color], + [600,700,color], + [600,600,color], + [100,100,0], + [100,100,color], + [200,100,color], + [200,200,color], + [100,200,color], + [100,100,color], + [0,0,0], + [0,0,color], + [800,0,color], + [800,800,color], + [0,800,color], + [0,0,color], + [350,400,0], + [350,400,color], + [450,400,color], + [400,350,0], + [400,350,color], + [400,450,color], +] + +shape = mire + + +while True: + start = time.time() + print(shape, flush=True); + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/generators/example.py b/generators/example.py new file mode 100755 index 0000000..b1ad01c --- /dev/null +++ b/generators/example.py @@ -0,0 +1,182 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +example, based on custom +v0.1.0 + +A copy of square.py you can modify to code your plugin. +custom1 has necessary hooks in LJ.conf, webui and so on. + + +LICENCE : CC + +by Sam Neurohack + + +''' +import sys +import os +ljpath = r'%s' % os.getcwd().replace('\\','/') + +# import from shell +sys.path.append(ljpath +'/../../libs/') + +#import from LJ +sys.path.append(ljpath +'/libs/') +print(ljpath+'/../libs/') + +import lj23layers as lj + +sys.path.append('../libs') +import math +import time +import argparse + + +print ("") +print ("Arguments parsing if needed...") +argsparser = argparse.ArgumentParser(description="Custom1 example for LJ") +argsparser.add_argument("-v","--verbose",help="Verbosity level (0 by default)",default=0,type=int) +args = argsparser.parse_args() + +# Useful variables init. +white = lj.rgb2int(255,255,255) +red = lj.rgb2int(255,0,0) +blue = lj.rgb2int(0,0,255) +green = lj.rgb2int(0,255,0) + +width = 800 +height = 600 +centerX = width / 2 +centerY = height / 2 + +# 3D to 2D projection parameters +fov = 256 +viewer_distance = 2.2 + +# Anaglyph computation parameters for right and left eyes. +# algorythm come from anaglyph geo maps +eye_spacing = 100 +nadir = 0.5 +observer_altitude = 30000 +map_layerane_altitude = 0.0 + +# square coordinates : vertices that compose each of the square. +vertices = [ + (- 1.0, 1.0,- 1.0), + ( 1.0, 1.0,- 1.0), + ( 1.0,- 1.0,- 1.0), + (- 1.0,- 1.0,- 1.0) + ] + +face = [0,1,2,3] + +# +# LJ inits +# + +layer = 0 + +# Define properties for each drawn "element" : name, intensity, active, xy, color, red, green, blue, layer , closed +Leftsquare = lj.FixedObject('Leftsquare', True, 255, [], red, 255, 0, 0, layer , True) +Rightsquare = lj.FixedObject('Rightsquare', True, 255, [], green, 0, 255, 0, layer , True) + +# 'Destination' for given layer : name, number, active, layer , scene, laser +Dest0 = lj.DestObject('0', 0, True, 0 , 0, 0) # Dest0 will send layer 0 points to scene 0, laser 0 + + +# +# Anaglyph computation : different X coordinate for each eye +# + +def LeftShift(elevation): + + diff = elevation - map_layerane_altitude + return nadir * eye_spacing * diff / (observer_altitude - elevation) + +def RightShift(elevation): + + diff = map_layerane_altitude - elevation + return (1 - nadir) * eye_spacing * diff / (observer_altitude - elevation) + + +def Proj(x,y,z,angleX,angleY,angleZ): + + rad = angleX * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + y2 = y + y = y2 * cosa - z * sina + z = y2 * sina + z * cosa + + rad = angleY * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + z2 = z + z = z2 * cosa - x * sina + x = z2 * sina + x * cosa + + rad = angleZ * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + x2 = x + x = x2 * cosa - y * sina + y = x2 * sina + y * cosa + + + """ Transforms this 3D point to 2D using a perspective projection. """ + factor = fov / (viewer_distance + z) + x = x * factor + centerX + y = - y * factor + centerY + return (x,y) + + +# +# Main +# + +def Run(): + Left = [] + Right = [] + counter =0 + try: + while True: + Left = [] + Right = [] + x = vertices[0][0] + y = vertices[0][1] + z = vertices[0][2] + + # lj tracers will "move" the laser to this first point in black, then move to the next with second point color. + # for more accuracy in dac emulator, repeat this first point. + + # generate all points in square. + for point in face: + x = vertices[point][0] + y = vertices[point][1] + z = vertices[point][2] + left.append(proj(x+leftshift(z*25),y,z,0,counter,0)) + right.append(proj(x+rightshift(z*25),y,z,0,counter,0)) + + + lj.polylineonecolor(left, c = leftsquare.color , layer = leftsquare.layer, closed = leftsquare.closed) + lj.polylineonecolor(right, c = rightsquare.color , layer = rightsquare.layer, closed = rightsquare.closed) + lj.drawdests() + time.sleep(0.1) + counter += 1 + if counter > 360: + counter = 0 + + except KeyboardInterrupt: + pass + + # Gently stop on CTRL C + finally: + lj.ClosePlugin() + + +Run() diff --git a/generators/fromGML.py b/generators/fromGML.py new file mode 100644 index 0000000..7576b8f --- /dev/null +++ b/generators/fromGML.py @@ -0,0 +1,406 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +fromGML +v0.1.0 + +Display a GML file + +See GML specs at the end. +Support the gml spec="1.0 (minimum)" +and header/client/name +and maybe one day drawing/brush/color + +LICENCE : CC +by cocoa and Sam Neurohack + +Heavy use of : https://github.com/kgn/pygml +''' +from __future__ import print_function +import time +import struct +import argparse +import sys +import xml.etree.ElementTree as etree +#import urllib +from datetime import datetime +import math, random +import ast + +name="generator::fromgml" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="GML file frame generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-g","--gml",help=".gml file",default="147.gml",type=str) +argsparser.add_argument("-t","--total",help="Total time",default=32,type=int) +argsparser.add_argument("-m","--mode",help="once or anim mode",default="anim",type=str) +argsparser.add_argument("-s","--skip",help="% of points to skip",default="0.4",type=float) +argsparser.add_argument("-r","--rot",help="(angleX, angleY, angleZ) in degree",default="(0,0,270)",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +mode = args.mode +optimal_looptime = 1 / fps +angles = ast.literal_eval(args.rot) +debug(name+" optimal frame time "+str(optimal_looptime)) + + +TOTAL_TIME=float(args.total) +TIME_STRETCH = 1 +ZOOM=1.0 +DELTA = 7 +width = 500 +height = 500 +centerX = width / 2 +centerY = height / 2 +# 3D to 2D projection parameters +fov = 200 +viewer_distance = 2.2 + +skip = args.skip +#skip is the percentage of points that we ignore in order to render +# faster in the laser display. Unfortunately we are not able to render too +# complex content in our display without resulting in a lot of blinking. + +# return a list with all points +def readGML(filename): + + outputData = [] + tree = etree.parse(filename) + root = tree.getroot() + ''' + if (root.tag.lower() != "gml"): + print("Not a GML file.") + return + ''' + #~ + tag = root.find("tag") + header = tag.find("header") + if header != None: + client = header.find("client") + if client != None: + debug("Graffiti name :", client.find("name").text) + drawing = tag.find("drawing") + environment = header.find("environment") + if not environment: + environment = tag.find("environment") + #screenBounds = environment.find("screenBounds") + #globalScale = (1.0,1.0,1.0) + #dim = (float(screenBounds.find("x").text) * globalScale[0], float(screenBounds.find("y").text) * globalScale[1], float(screenBounds.find("z").text) * globalScale[2]) + #dim = (40.0,40.0,40.0) + #~ + strokes = drawing.findall("stroke") + for stroke in strokes: + pointsEl = stroke.findall("pt") + for pointEl in pointsEl: + + x = float(pointEl.find("x").text) - 0.5 + y = float(pointEl.find("y").text) - 0.5 + z = float(pointEl.find("z").text) - 0.5 + transpoint = Rot(x,y,z,angles[0],angles[1],angles[2]) + x = (transpoint[0]*ZOOM*width/2) + (width/2) + y = (transpoint[1]*ZOOM*height/2) + (height/2) + z = transpoint[2] + # WIDTH/2 + ZOOM*point[0]*WIDTH/2, HEIGHT/2 + ZOOM*point[1]*HEIGHT/2 + time = float(pointEl.find("time").text) + outputData.append([x,y,z,time]) + #print(outputData) + return outputData + +def Rot(x, y, z, angleX, angleY, angleZ): + + rad = angleX * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + y2 = y + y = y2 * cosa - z * sina + z = y2 * sina + z * cosa + + rad = angleY * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + z2 = z + z = z2 * cosa - x * sina + x = z2 * sina + x * cosa + + rad = angleZ * math.pi / 180 + cosa = math.cos(rad) + sina = math.sin(rad) + x2 = x + x = x2 * cosa - y * sina + y = x2 * sina + y * cosa + + return (x,y,z) + + +#[x,y,z,time] +def iterPoints(): + + for point in gml: + yield point + + +# Play once during total time arg +def Once(): + + debug(name,"play once mode") + shape = [] + for point in gml: + shape.append([point[0],point[1], 65535]) + debug(name + str(shape)) + + t0=datetime.now() + deltat=0 + + while deltat TOTAL_TIME: + t0=datetime.now() + + first=True + shape = [] + for point in iterPoints(): + if point[3] <= deltat and deltat <= point[3]+DELTA and random.random()<(1-skip): + if first: + first=False + else: + #LD.draw_point(WIDTH/2 + ZOOM*point.x*WIDTH/2, HEIGHT/2 + ZOOM*point.y*HEIGHT/2) + shape.append([point[0], point[1], 65535]) + print(shape, flush=True); + + +debug(name + " Reading : "+args.gml+" in "+mode+" mode.") +gml = readGML(args.gml) + +debug(name + " total points : "+ str(len(gml))) + +if mode =="once": + Once() +else: + Anim() + +debug(name + " ends.") +exit() +''' + + + + + + + + 0.0 + 0.0 + + + + + + + + + + + + + +
+ + + Laser Tag + 2.0 + MyUserName + http://000000book.com/data/156/ + katsu,paris,2010 + 28sks922ks992 + 192.168.1.1 + + + -39.392922 + 53.29292 + + + + + + + + + + 0.0 + 0.0 + 0.0 + + + 0.0 + 0.0 + 0.0 + + + 0.0 + -1.0 + 0.0 + + + 1024 + 768 + 0 + + + 0 + 0 + 0 + + + 1000 + 600 + 0 + cm + + + yourimage.jpg + + +
+ + + + + + + + 0.0 + 0.0 + 0.0 + 0.013 + + + + + + + + + + 0 + LaserTagArrowLetters + + http://aurltodescribethebrushspec.com/someSpec.xml + 10 + 1.5 + 1.0 + 1.0 + 0 + + 255 + 255 + 255 +
255 + + + 0 + 1 + 0 + + + + + 0.0 + 0.0 + 0.0 + 0.013 + + + + 0.0 + 0.0 + 0.0 + 0.023 + + + + + + + + + + true + + + + 255 + 255 + 0 + + + -1 + + + + + 0.0 + 0.0 + + + + 0.0 + 0.0 + + + + + + + 0.5 + 0.5 + + + + + + + + + + + + +''' \ No newline at end of file diff --git a/generators/fromOSC.py b/generators/fromOSC.py new file mode 100644 index 0000000..d8005c5 --- /dev/null +++ b/generators/fromOSC.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +Forward /pl pointlist to cli + +input OSC in END points format : (x,y,color) +output CLI in CLI points format : [x,y,color] + +/pl "[(150.0, 230.0, 255), (170.0, 170.0, 255), (230.0, 170.0, 255), (210.0, 230.0, 255), (150.0, 230.0, 255)]" + +v0.1.0 + +LICENCE : CC + +by Cocoa, Sam Neurohack + +''' +from __future__ import print_function +from OSC3 import OSCServer, OSCClient, OSCMessage +import sys +from time import sleep +import argparse +import ast + +argsparser = argparse.ArgumentParser(description="fromOSC generator") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="OSC port to bind to (9002 by default)",default=9002,type=str) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +oscserver = OSCServer( (ip, port) ) +oscserver.timeout = 0 +run = True + +# this method of reporting timeouts only works by convention +# that before calling handle_request() field .timed_out is +# set to False +def handle_timeout(self): + self.timed_out = True + +# funny python's way to add a method to an instance of a class +import types +oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver) + +# RAW OSC Frame available ? +def OSC_frame(): + # clear timed_out flag + oscserver.timed_out = False + # handle all pending requests then return + while not oscserver.timed_out: + oscserver.handle_request() + + +# default handler +def OSChandler(oscpath, tags, args, source): + + oscaddress = ''.join(oscpath.split("/")) + debug("fromOSC Default OSC Handler got oscpath", oscpath, "from" + str(source[0]), ":", args) + #print("OSC address", path) + #print("find.. /bhoreal ?", path.find('/bhoreal')) + + if oscpath == "/pl" and len(args)==1: + + debug("correct OSC type :'/pl") + + if validate(args[0]) == True: + + debug("new pl : ", args[0]) + line = args[0].replace("(",'[') + line = line.replace(")",']') + line = "[{}]".format(line) + print(line, flush=True); + + else: + debug("Bad pointlist -> msg trapped.") + + else: + debug("BAD OSC Message : " + oscpath +" " +args[0]) + + +oscserver.addMsgHandler( "default", OSChandler ) + + +def validate(pointlist): + + state = True + + if len(pointlist)<9: + debug("Not enough characters :", pointlist) + state = False + + if pointlist.find("(") == -1: + debug("Bad format : use () not [] for points", pointlist) + state = False + + try: + pl = bytes(pointlist, 'ascii') + check = ast.literal_eval(pl.decode('ascii')) + + except: + debug("BAD POINTLIST :", pointlist) + state = False + + return state + + +# simulate a "game engine" +while run: + # do the game stuff: + sleep(0.01) + # call user script + OSC_frame() + +oscserver.close() + diff --git a/generators/fromRedis.py b/generators/fromRedis.py new file mode 100755 index 0000000..8c65951 --- /dev/null +++ b/generators/fromRedis.py @@ -0,0 +1,72 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +This generator reads a frame from redis +v0.1.0 + +Use it to create feedback loops by writing to the same frame +or to copy the frame from someone else + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import ast +import argparse +import json +import redis +import sys +import time +name="generator::fromRedis" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="Dummy generator") +argsparser.add_argument("-k","--key",required=True,help="Redis key to look after",default=30,type=str) +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +fps = args.fps +verbose = args.verbose +key = args.key +ip = args.ip +port = args.port +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +r = redis.Redis( + host=ip, + port=port) + +while True: + start = time.time() + # Read from Redis + line = r.get(key) + # Decode as list of tuples + pointsList = ast.literal_eval(line.decode('ascii')) + # convert to list of lists + pointsList = [list(elem) for elem in pointsList] + # Convert to JSON string + line = json.dumps( pointsList ) + debug(name,"Key:{} line:{}".format(key,line)) + print(line, flush=True); + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + diff --git a/generators/fromUDP.py b/generators/fromUDP.py new file mode 100644 index 0000000..5f6e5e9 --- /dev/null +++ b/generators/fromUDP.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +fromUDP + +Udp server to cli +v0.1b + +''' +from __future__ import print_function +import traceback, time +import argparse +import socket +import _thread +import sys + +name="generator::fromUDP" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="fromUDP v0.1b help mode") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="UDP port to bind to (9000 by default)",default=9000,type=str) +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) +verbose = args.verbose + + + +def udp_thread(): + + while True: + + payload, client_address = sock.recvfrom(1024) + udpath = payload.decode('utf_8') + debug(udpath[0:]) + print(udpath[0:], flush=True); + + ''' + # Reply to client + bytesToSend = str.encode("ACK :"+str(payload)) + serverAddressPort = (client_address, port) + bufferSize = 1024 + #sock.sendto(bytesToSend, serverAddressPort) + sock.sendto(bytesToSend, client_address) + ''' + +def StartUDP(serverIP, UDPORT): + global sock + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + server = ( serverIP,UDPORT) + sock.bind(server) + _thread.start_new_thread(udp_thread, ()) + + +StartUDP(ip, port) + + +# Do something else +try: + + while True: + time.sleep(0.005) + +except Exception: + traceback.print_exc() + + + + + diff --git a/generators/fromilda.py b/generators/fromilda.py new file mode 100644 index 0000000..e6547e5 --- /dev/null +++ b/generators/fromilda.py @@ -0,0 +1,319 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +fromild +v0.1.0 + +Read/display once an .ild animation file and quit ?? +LICENCE : CC + +by cocoa and Sam Neurohack + +Heavy u-se of : + +ILDA.py + +Python module for dealing with the ILDA Image Data Transfer Format, +an interchange format for laser image frames. + +Copyright (c) 2008 Micah Dowty + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +''' + +from __future__ import print_function +import time +import struct +import argparse +import sys + +name="generator::fromild" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description=".ild file frame generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--ild",help=".ild file",default="book2.ild",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +# Format codes +FORMAT_3D = 0 +FORMAT_2D = 1 +FORMAT_COLOR_TABLE = 2 + +# Mapping from FORMAT_* codes to struct format strings +formatTable = ( + '>hhhH', + '>hhH', + '>BBB', + ) + +# Header values +HEADER_MAGIC = b"ILDA\0\0\0" +HEADER_RESERVED = 0 +HEADER_FORMAT = ">7sB16sHHHBB" +HEADER_LEN = struct.calcsize(HEADER_FORMAT) + +# 64 default colors table : use rgb2int(colors64[ildacolor]) +colors64 = [[255, 0, 0], [255, 17, 0], [255, 34, 0], [255, 51, 0], [255, 68, 0], [255, 85, 0], [255, 102, 0], [255, 119, 0], [255, 136, 0], [255, 153, 0], [255, 170, 0], [255, 187, 0], [255, 204, 0], [255, 221, 0], [255, 238, 0], [255, 255, 0], [255, 255, 0], [238, 255, 0], [204, 255, 0], [170, 255, 0], [136, 255, 0], [102, 255, 0], [68, 255, 0], [34, 255, 0], [0, 255, 0], [0, 255, 34], [0, 255, 68], [0, 255, 102], [0, 255, 136], [0, 255, 170], [0, 255, 204], [0, 255, 238], [0, 136, 255], [0, 119, 255], [0, 102, 255], [0, 102, 255], [0, 85, 255], [0, 68, 255], [0, 68, 255], [0, 34, 255], [0, 0, 255], [34, 0, 255], [68, 0, 255], [102, 0, 255], [136, 0, 255], [170, 0, 255], [204, 0, 255], [238, 0, 255], [255, 0, 255], [255, 34, 255], [255, 68, 255], [255, 102, 255], [255, 136, 255], [255, 170, 255], [255, 204, 255], [255, 238, 255], [255, 255, 255], [255, 238, 238], [255, 204, 204], [255, 170, 170], [255, 136, 136], [255, 102, 102], [255, 68, 68], [0, 34, 34]] + + +# 256 default colors table +colors256 = [[0, 0, 0], [255, 255, 255], [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 128, 128], [255, 140, 128], [255, 151, 128], [255, 163, 128], [255, 174, 128], [255, 186, 128], [255, 197, 128], [255, 209, 128], [255, 220, 128], [255, 232, 128], [255, 243, 128], [255, 255, 128], [243, 255, 128], [232, 255, 128], [220, 255, 128], [209, 255, 128], [197, 255, 128], [186, 255, 128], [174, 255, 128], [163, 255, 128], [151, 255, 128], [140, 255, 128], [128, 255, 128], [128, 255, 140], [128, 255, 151], [128, 255, 163], [128, 255, 174], [128, 255, 186], [128, 255, 197], [128, 255, 209], [128, 255, 220], [128, 255, 232], [128, 255, 243], [128, 255, 255], [128, 243, 255], [128, 232, 255], [128, 220, 255], [128, 209, 255], [128, 197, 255], [128, 186, 255], [128, 174, 255], [128, 163, 255], [128, 151, 255], [128, 140, 255], [128, 128, 255], [140, 128, 255], [151, 128, 255], [163, 128, 255], [174, 128, 255], [186, 128, 255], [197, 128, 255], [209, 128, 255], [220, 128, 255], [232, 128, 255], [243, 128, 255], [255, 128, 255], [255, 128, 243], [255, 128, 232], [255, 128, 220], [255, 128, 209], [255, 128, 197], [255, 128, 186], [255, 128, 174], [255, 128, 163], [255, 128, 151], [255, 128, 140], [255, 0, 0], [255, 23, 0], [255, 46, 0], [255, 70, 0], [255, 93, 0], [255, 116, 0], [255, 139, 0], [255, 162, 0], [255, 185, 0], [255, 209, 0], [255, 232, 0], [255, 255, 0], [232, 255, 0], [209, 255, 0], [185, 255, 0], [162, 255, 0], [139, 255, 0], [116, 255, 0], [93, 255, 0], [70, 255, 0], [46, 255, 0], [23, 255, 0], [0, 255, 0], [0, 255, 23], [0, 255, 46], [0, 255, 70], [0, 255, 93], [0, 255, 116], [0, 255, 139], [0, 255, 162], [0, 255, 185], [0, 255, 209], [0, 255, 232], [0, 255, 255], [0, 232, 255], [0, 209, 255], [0, 185, 255], [0, 162, 255], [0, 139, 255], [0, 116, 255], [0, 93, 255], [0, 70, 255], [0, 46, 255], [0, 23, 255], [0, 0, 255], [23, 0, 255], [46, 0, 255], [70, 0, 255], [93, 0, 255], [116, 0, 255], [139, 0, 255], [162, 0, 255], [185, 0, 255], [209, 0, 255], [232, 0, 255], [255, 0, 255], [255, 0, 232], [255, 0, 209], [255, 0, 185], [255, 0, 162], [255, 0, 139], [255, 0, 116], [255, 0, 93], [255, 0, 70], [255, 0, 46], [255, 0, 23], [128, 0, 0], [128, 12, 0], [128, 23, 0], [128, 35, 0], [128, 47, 0], [128, 58, 0], [128, 70, 0], [128, 81, 0], [128, 93, 0], [128, 105, 0], [128, 116, 0], [128, 128, 0], [116, 128, 0], [105, 128, 0], [93, 128, 0], [81, 128, 0], [70, 128, 0], [58, 128, 0], [47, 128, 0], [35, 128, 0], [23, 128, 0], [12, 128, 0], [0, 128, 0], [0, 128, 12], [0, 128, 23], [0, 128, 35], [0, 128, 47], [0, 128, 58], [0, 128, 70], [0, 128, 81], [0, 128, 93], [0, 128, 105], [0, 128, 116], [0, 128, 128], [0, 116, 128], [0, 105, 128], [0, 93, 128], [0, 81, 128], [0, 70, 128], [0, 58, 128], [0, 47, 128], [0, 35, 128], [0, 23, 128], [0, 12, 128], [0, 0, 128], [12, 0, 128], [23, 0, 128], [35, 0, 128], [47, 0, 128], [58, 0, 128], [70, 0, 128], [81, 0, 128], [93, 0, 128], [105, 0, 128], [116, 0, 128], [128, 0, 128], [128, 0, 116], [128, 0, 105], [128, 0, 93], [128, 0, 81], [128, 0, 70], [128, 0, 58], [128, 0, 47], [128, 0, 35], [128, 0, 23], [128, 0, 12], [255, 192, 192], [255, 64, 64], [192, 0, 0], [64, 0, 0], [255, 255, 192], [255, 255, 64], [192, 192, 0], [64, 64, 0], [192, 255, 192], [64, 255, 64], [0, 192, 0], [0, 64, 0], [192, 255, 255], [64, 255, 255], [0, 192, 192], [0, 64, 64], [192, 192, 255], [64, 64, 255], [0, 0, 192], [0, 0, 64], [255, 192, 255], [255, 64, 255], [192, 0, 192], [64, 0, 64], [255, 96, 96], [255, 255, 255], [245, 245, 245], [235, 235, 235], [224, 224, 224], [213, 213, 213], [203, 203, 203], [192, 192, 192], [181, 181, 181], [171, 171, 171], [160, 160, 160], [149, 149, 149], [139, 139, 139], [128, 128, 128], [117, 117, 117], [107, 107, 107], [96, 96, 96], [85, 85, 85], [75, 75, 75], [64, 64, 64], [53, 53, 53], [43, 43, 43], [32, 32, 32], [21, 21, 21], [11, 11, 11], [0, 0, 0]] + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +class Table(object): + """Container object for one ILDA table: either a frame (table of points) + or a palette (table of colors). + + The 'items' list contains the data within this table. Each item + is a tuple, corresponding to the raw values within that row of the + table. + + 2D frame: (x, y, status) + 3D frame: (x, y, z, status) + Color: (r, g, b) + + """ + def __init__(self, format=FORMAT_2D, name="", + length=0, number=0, total=0, scanHead=0): + self.__dict__.update(locals()) + self.items = [] + self.itemsproducer = None + + def __repr__(self): + return ("" % + (self.format, self.name, self.length, self.number, + self.total, self.scanHead)) + + def unpackHeader(self, data): + magic, self.format, self.name, self.length, \ + self.number, self.total, self.scanHead, \ + reserved = struct.unpack(HEADER_FORMAT, data) + print(magic, HEADER_MAGIC) + if magic != HEADER_MAGIC: + raise ValueError("Bad ILDA header magic. Not an ILDA file?") + if reserved != HEADER_RESERVED: + raise ValueError("Reserved ILDA field is not zero.") + + def packHeader(self): + return struct.pack(HEADER_FORMAT, HEADER_MAGIC, self.format, + self.name, self.length, self.number, + self.total, self.scanHead, HEADER_RESERVED) + + def readHeader(self, stream): + self.unpackHeader(stream.read(HEADER_LEN)) + + def writeHeader(self, stream): + stream.write(self.packHeader()) + + def _getItemFormat(self): + try: + return formatTable[self.format] + except IndexError: + raise ValueError("Unsupported format code") + + def read_stream(self, stream): + """Read the header, then read all items in this table.""" + self.readHeader(stream) + if self.length: + fmt = self._getItemFormat() + itemSize = struct.calcsize(fmt) + self.items = [struct.unpack(fmt, stream.read(itemSize)) + for i in range(self.length)] + self.itemsproducer = self.produce() + + def write(self, stream): + """Write the header, then write all items in this table.""" + self.writeHeader(stream) + if self.length: + fmt = self._getItemFormat() + itemSize = struct.calcsize(fmt) + stream.write(''.join([struct.pack(fmt, *item) + for item in self.items])) + + def iterPoints(self): + """Iterate over Point instances for each item in this table. + Only makes sense if this is a 2D or 3D point table. + """ + for item in self.items: + p = Point() + p.decode(item) + yield p + + + def produce(self): + """Iterate over Point instances for each item in this table. + Only makes sense if this is a 2D or 3D point table. + """ + while True: + for item in self.items: + p = Point() + p.decode(item) + yield p.encode() + #yield (p.x, p.y, p.z, p.color, p.blanking) + + def read(self, cap): + """yields what dac.play_stream() needs (x, y, z, ?, ?) + """ + return [next(self.itemsproducer) for i in range(cap)] + + +class Point: + """Abstraction for one vector point. The Table object, for + completeness and efficiency, stores raw tuples for each + point. This is a higher level interface that decodes the status + bits and represents coordinates in floating point. + """ + def __init__(self, x=0.0, y=0.0, z=0.0, color=0, blanking=False): + self.__dict__.update(locals()) + + def __repr__(self): + + return "%s, %s, %s, %s, %s" % ( + self.x, self.y, self.z, self.color, self.blanking) + #return "" % ( + # self.x, self.y, self.z, self.color, self.blanking) + + def encode(self): + status = self.color & 0xFF + if self.blanking: + status |= 1 << 14 + + return ( + int( min(0x7FFF, max(-0x7FFF, self.x * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.y * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.z * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.color * 0x7FFF)) ), + int( min(0x7FFF, max(-0x7FFF, self.blanking * 0x7FFF)) ) + ) + + def decode(self, t): + #print "~~ Decoding, t of len "+ str(len(t)) +" is: " + str(t) + self.x = t[0] / 0x7FFF + self.y = t[1] / 0x7FFF + if len(t) > 3: + self.z = t[2] / 0x7FFF + # self.color = t[3] & 0xFF + # self.blanking = (t[3] & (1 << 14)) != 0 + else: + self.z = 0.0 + + self.color = t[-1] & 0xFF + self.blanking = (t[-1] & (1 << 14)) != 0 + + +def read(stream): + """Read ILDA data from a stream until we hit the + end-of-stream marker. Yields a sequence of Table objects. + """ + while True: + t = Table() + t.read_stream(stream) + if not t.length: + # End-of-stream + break + yield t + + +def write(stream, tables): + """Write a sequence of tables in ILDA format, + terminated by an end-of-stream marker. + """ + for t in tables: + t.write(stream) + Table().write(stream) + + +def readFrames(stream): + """Read ILDA data from a stream, and ignore + all non-frame tables. Yields only 2D or 3D + point tables. + """ + for t in read(stream): + if t.format in (FORMAT_2D, FORMAT_3D): + yield t + + +def readFirstFrame(stream): + """Read only a single frame from an ILDA stream.""" + for frame in readFrames(stream): + return frame + + +# +f = open(args.ild, 'rb') +myframe = readFirstFrame(f) + +while myframe.number +1< myframe.total: + + start = time.time() + shape =[] + + if myframe is None: + f.close() + break + + debug(name,"Frame", myframe.number, "/",myframe.total, "length", myframe.length) + + for p in myframe.iterPoints(): + p2 = str(p) + point = p2.split(',') + + x = float(point[0]) + y = float(point[1]) + z = float(point[2]) + color = int(point[3]) + blanking = point[4][1:] + + if blanking == "True": + shape.append([300+(x*300),300+(-y*300),0]) + else: + shape.append([300+(x*300),300+(-y*300),rgb2int(colors64[color])]) + + print(shape, flush=True); + myframe = readFirstFrame(f) + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + +f.close() +debug(name + " end of .ild animation") diff --git a/generators/osc2redis.py b/generators/osc2redis.py new file mode 100644 index 0000000..af8a73e --- /dev/null +++ b/generators/osc2redis.py @@ -0,0 +1,131 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +Forward pointlist to redis key + +END POINT Format : (x,y,color) + +/pl/0/0 "[(150.0, 230.0, 255), (170.0, 170.0, 255), (230.0, 170.0, 255), (210.0, 230.0, 255), (150.0, 230.0, 255)]" + +v0.1.0 + +LICENCE : CC + +by Cocoa, Sam Neurohack + +''' + +from OSC3 import OSCServer, OSCClient, OSCMessage +import sys +from time import sleep +import argparse +import ast +import redis + +argsparser = argparse.ArgumentParser(description="osc2redis generator") +argsparser.add_argument("-i","--ip",help="IP to bind to (0.0.0.0 by default)",default="0.0.0.0",type=str) +argsparser.add_argument("-p","--port",help="OSC port to bind to (9002 by default)",default=9002,type=str) + +argsparser.add_argument("-r","--rip",help="Redis server IP (127.0.0.1 by default)",default="127.0.0.1",type=str) +argsparser.add_argument("-o","--rout",help="Redis port (6379 by default)",default=6379,type=str) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +args = argsparser.parse_args() + +verbose = args.verbose +ip = args.ip +port = int(args.port) +rip = args.rip +rport = int(args.rout) + + +r = redis.StrictRedis(host=rip, port=rport, db=0) + + +def debug(msg): + if( verbose == False ): + return + print(msg) + + +oscserver = OSCServer( (ip, port) ) +oscserver.timeout = 0 +run = True + +# this method of reporting timeouts only works by convention +# that before calling handle_request() field .timed_out is +# set to False +def handle_timeout(self): + self.timed_out = True + +# funny python's way to add a method to an instance of a class +import types +oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver) + + +def validate(pointlist): + + state = True + + if len(pointlist)<9: + state = False + + try: + pl = bytes(pointlist, 'ascii') + check = ast.literal_eval(pl.decode('ascii')) + + except: + state = False + + return state + + +# RAW OSC Frame available ? +def OSC_frame(): + # clear timed_out flag + oscserver.timed_out = False + # handle all pending requests then return + while not oscserver.timed_out: + oscserver.handle_request() + + +# default handler +def OSChandler(oscpath, tags, args, source): + + oscaddress = ''.join(oscpath.split("/")) + print("fromOSC Default OSC Handler got oscpath :", oscpath, "from :" + str(source[0]), "args :", args) + print(oscpath.find("/pl/"), len(oscpath)) + + if oscpath.find("/pl/") ==0 and len(args)==1: + + print("correct OSC type :'/pl/") + + if validate(args[0]) == True and len(oscpath) == 7: + print("new pl for key ", oscpath, ":", args[0]) + + if r.set(oscpath,args[0])==True: + debug("exports::redis set("+str(oscpath)+") to "+args[0]) + + else: + print("Bad pointlist -> msg trapped.") + + + else: + print("BAD OSC Message :", oscpath) + +oscserver.addMsgHandler( "default", OSChandler ) + + + + +# simulate a "game engine" +while run: + # do the game stuff: + sleep(0.01) + # call user script + OSC_frame() + +oscserver.close() + diff --git a/generators/redilysis_lines.py b/generators/redilysis_lines.py new file mode 100755 index 0000000..123f487 --- /dev/null +++ b/generators/redilysis_lines.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +redilysis_lines +v0.1.0 + +Add a line on every frame and scroll + +see https://git.interhacker.space/teamlaser/redilysis for more informations +about the redilysis project + +LICENCE : CC + +by cocoa + + +''' +from __future__ import print_function +import argparse +import ast +import os +import math +import random +import redis +import sys +import time +name = "generator::redilysis_lines" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) +def msNow(): + return time.time() + +CHAOS = 1 +REDIS_FREQ = 33 + +# General Args +argsparser = argparse.ArgumentParser(description="Redilysis filter") +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-F","--redis-freq",help="Query Redis every x (in milliseconds). Default:{}".format(REDIS_FREQ),default=REDIS_FREQ,type=int) +# General args +argsparser.add_argument("-n","--nlines",help="number of lines on screen",default=60,type=int) +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) +argsparser.add_argument("-W","--max-width",help="geometrical max width",default=800,type=int) +argsparser.add_argument("-H","--max-height",help="geometrical max height",default=800,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) + +args = argsparser.parse_args() +verbose = args.verbose +ip = args.ip +port = args.port +fps = args.fps +centerX = args.centerX +centerY = args.centerY +redisFreq = args.redis_freq / 1000 +maxWidth = args.max_width +maxHeight = args.max_height +nlines = args.nlines +optimal_looptime = 1 / fps + +redisKeys = ["spectrum_120","spectrum_10"] +debug(name,"Redis Keys:{}".format(redisKeys)) +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +white = 16777215 +lineList = [] + +scroll_speed = int(maxHeight / nlines ) +line_length = int(maxWidth / 10) +line_pattern = [] + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def spectrum_10( ): + delList = [] + spectrum = ast.literal_eval(redisData["spectrum_10"]) + debug( name, "spectrum:{}".format(spectrum)) + # scroll lines + for i,line in enumerate(lineList): + skip_line = False + new_y = int(line[0][1] + scroll_speed) + if( new_y >= maxHeight ): + debug(name,"{} > {}".format(new_y,maxHeight)) + debug(name,"delete:{}".format(i)) + delList.append(i) + continue + + for j,point in enumerate(line): + line[j][1] = new_y + lineList[i] = line + + for i in delList: + del lineList[i] + + # new line + currentLine = [] + for i in range(0,10): + x = int(i * line_length) + y = 0 + # get frequency level + level = spectrum[i] + # get color + comp = int(255*level) + color = rgb2int( (comp,comp,comp)) + # new point + currentLine.append( [x,y,color] ) + + # add line to list + lineList.append( currentLine) + + +def refreshRedis(): + global redisLastHit + global redisData + # Skip if cache is sufficent + diff = msNow() - redisLastHit + if diff < redisFreq : + #debug(name, "refreshRedis not updating redis, {} < {}".format(diff, redisFreq)) + pass + else: + #debug(name, "refreshRedis updating redis, {} > {}".format(diff, redisFreq)) + redisLastHit = msNow() + for key in redisKeys: + redisData[key] = r.get(key).decode('ascii') + #debug(name,"refreshRedis key:{} value:{}".format(key,redisData[key])) + # Only update the TTLs + if 'bpm' in redisKeys: + redisData['bpm_pttl'] = r.pttl('bpm') + #debug(name,"refreshRedis key:bpm_ttl value:{}".format(redisData["bpm_pttl"])) + #debug(name,"redisData:{}".format(redisData)) + return True + +def linelistToPoints( lineList ): + pl = [] + for i,line in enumerate(lineList): + # add a blank point + pl.append([ line[0][0], line[0][1], 0 ]) + # append all the points of the line + pl += line + #debug(name,"pl:{}".format(pl)) + debug(name,"pl length:{}".format(len(pl))) + return pl + +try: + while True: + refreshRedis() + start = time.time() + # Do the thing + pointsList = spectrum_10() + print( linelistToPoints(lineList), flush=True ) + looptime = time.time() - start + # debug(name+" looptime:"+str(looptime)) + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + # debug(name+" micro sleep:"+str( optimal_looptime - looptime)) +except EOFError: + debug(name+" break")# no more information + diff --git a/generators/redilysis_particles.py b/generators/redilysis_particles.py new file mode 100755 index 0000000..59e775a --- /dev/null +++ b/generators/redilysis_particles.py @@ -0,0 +1,288 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +v0.1.0 + + +LICENCE : CC + +by cocoa + +''' +from __future__ import print_function +import math +import random +import sys +import os +import time +import redis +import ast +import argparse + + +MAX_PARTICLES = 50 +MAX_TIME = 500 + +argsparser = argparse.ArgumentParser(description="Dummy generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +# Redis Args +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +# +argsparser.add_argument("-M","--max-particles",help="Max Particles. Default:{}".format(MAX_PARTICLES),default=MAX_PARTICLES,type=int) +argsparser.add_argument("-m","--max-time",help="Max Particles. Default:{}".format(MAX_TIME),default=MAX_TIME,type=int) + + +args = argsparser.parse_args() +verbose = args.verbose +ip = args.ip +port = args.port +max_particles = args.max_particles +max_time = args.max_time + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def spectrum_120( ): + return ast.literal_eval(redisData["spectrum_10"]) + + +def rgb2int(rgb): + #debug(name,"::rgb2int rbg:{}".format(rgb)) + return int('0x%02x%02x%02x' % tuple(rgb),0) + +def msNow(): + return time.time() + +def refreshRedis(): + global redisData + for key in redisKeys: + redisData[key] = ast.literal_eval(r.get(key).decode('ascii')) + +name="generator::redilisys_particles" + +class UnpreparedParticle(Exception): + pass + +class Particle(object): + def __init__(self, x, y, m): + self.x = x + self.y = y + self.m = m + self.dx = 0 + self.dy = 0 + self.connectedTo = [] + + self.decay = random.randint(10,max_time) + self.color = (random.randint(128,256) - int(12.8 * self.m), + random.randint(128,256) - int(12.8 * self.m), + random.randint(128,256) - int(12.8 * self.m)) + self.color = (255,255,255) + #debug( self.color ) + + def interact(self, bodies): + self.connectedTo = [] + spec = redisData["spectrum_10"] + power = int(sum(spec[4:6])) + for other in bodies: + if other is self: + continue + dx = other.x - self.x + dy = other.y - self.y + dist = math.sqrt(dx*dx + dy*dy) + if dist == 0: + dist = 1 + if dist < 100 and random.randint(0,power) > 0.5 : + self.connectedTo.append(other) + self.decay += 2 + factor = other.m / dist**2 + high_power = sum(spec[8:9]) if sum(spec[8:9]) != 0 else 0.01 + self.dx += (dx * factor * self.m) + self.dy += (dy * factor * self.m) + #print "factor %f" % (factor,) + + def move(self): + spec = redisData["spectrum_10"] + x_friction = (2.2-(1+spec[7]/2)) + y_friction = (2.2-(1+spec[7]/2)) + #x_friction = 1.02 + #y_friction = 1.04 + self.dx /= x_friction if x_friction != 0 else 0.01 + self.dy /= y_friction if y_friction != 0 else 0.01 + self.x += self.dx + self.y += self.dy + if self.x > max_width: + self.dx = - self.dx /8 + self.x = max_width + if self.x < 1: + self.dx = - self.dx /8 + self.x = 1 + if self.y > max_height: + self.dy = - self.dy /4 + self.y = max_height + if self.y < 1: + self.dy = - self.dy /4 + self.y = 1 + #print "(%.2f,%.2f) -> (%.2f,%.2f)" % (ox, oy, self.x, self.y) + + def attractor(self,attractor): + spec = redisData["spectrum_10"] + power = sum(spec[0:4])/3 + # If we're going in the direction of center, reverse + next_x = self.x + self.dx + next_y = self.y + self.dy + next_dx = attractor["x"] - self.x + next_dy = attractor["y"] - self.y + next_dist = math.sqrt(next_dx*next_dx + next_dy*next_dy) + + dx = attractor["x"] - self.x + dy = attractor["y"] - self.y + dist = math.sqrt(dx*dx + dy*dy) + if dist == 0: + dist = 1 + factor = power/ dist**2 + x_acceleration = (dx * factor * power * power) + y_acceleration = (dx * factor * power * power) + + + if next_dist > dist: + self.dx -= x_acceleration * power + self.dy -= y_acceleration * power + else: + self.dx += x_acceleration + self.dy += y_acceleration + + +class Attractor(Particle): + def move(self): + pass + + +class ParticleViewer(object): + def __init__(self, particles, size=(800,800)): + (self.width, self.height) = size + self.size = size + self.particles = particles + self.xoff = 0 + self.yoff = 0 + self.scalefactor = 1 + + def redraw(self): + + pl = [] + drawnVectors = [] + for p in self.particles: + x = int(self.scalefactor * p.x) - self.xoff + y = int(self.scalefactor * p.y) - self.yoff + if x > max_width: + x = max_width + if x < 1: + x = 1 + if y > max_height: + y = max_height + if y < 1: + y = 1 + + color = rgb2int(p.color) + pl.append([x+1,y+1,0]) + pl.append([x+1,y+1,color]) + pl.append([x,y,color]) + + for other in p.connectedTo: + + if [other,self] in drawnVectors: + continue + drawnVectors.append([other,self]) + pl.append([x,y,0]) + pl.append([x,y,color]) + pl.append([other.x,other.y,color]) + + print(pl,flush = True) + + def decayParticles(self): + for i,p in enumerate(self.particles): + # Handle positional decay + if p.decay == 0: + del self.particles[i] + continue + p.decay = p.decay - 1 + # Handle color decay + n = int(255 * (p.decay / max_time )) + p.color = (n,n,n) + + + def emitParticles(self): + spec = redisData["spectrum_10"] + power = sum(spec[6:]) + if len(self.particles ) > math.sqrt(max_particles): + if len(self.particles) > max_particles: + return + if random.random() > power: + return + # x is either left or right + d = 600 + rx = 100 if random.randint(0,1) else 700 + #rx = random.randint(1,max_width) + ry = random.randint(1,max_height) + spec = redisData["spectrum_10"] + m = random.randint(1,1+int(10*spec[7])) + particles.append(Particle(rx, ry, m)) + + + def tick(self): + self.decayParticles() + self.emitParticles() + for p in self.particles: + p.interact(self.particles) + p.attractor({ + "x":max_width/2, + "y":max_height/2 + }) + for p in particles: + p.move() + self.redraw() + + def scale(self, factor): + self.scalefactor += factor + + + + +max_width = 800 +max_height = 800 +redisKeys = ["spectrum_120","spectrum_10"] +redisData = {} +redisLastHit = msNow() - 99999 +r = redis.Redis( + host=ip, + port=port) + +white = 16777215 + +refreshRedis() +if __name__ == "__main__": + particles = [] +# particles.append(Attractor(320, 200, 10)) +# particles.append(Attractor(100, 100, 10)) + + win = ParticleViewer(particles) + try: + while True: + win.tick() + refreshRedis() + time.sleep(.03) + + except KeyboardInterrupt: + pass diff --git a/generators/text.py b/generators/text.py new file mode 100644 index 0000000..63fbcbd --- /dev/null +++ b/generators/text.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +Experimental Laserized Turtle graphics library + +See turtle1.py for example + +pip3 install Hershey-Fonts + +v0.1.0 + +Font list : +'futural', 'astrology', 'cursive', 'cyrilc_1', 'cyrillic', 'futuram', 'gothgbt', 'gothgrt', +'gothiceng', 'gothicger', 'gothicita', 'gothitt', 'greek', 'greekc', 'greeks', 'japanese', +'markers', 'mathlow', 'mathupp', 'meteorology', 'music', 'rowmand', 'rowmans', 'rowmant', +'scriptc', 'scripts', 'symbolic', 'timesg', 'timesi', 'timesib', 'timesr', 'timesrb' + +LICENCE : CC + +by cocoa and Sam Neurohack + +''' + +from __future__ import print_function +import time +import argparse +import sys +from HersheyFonts import HersheyFonts + +name="generator::text" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="Text generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-t","--text",help="Text to display",default="hello",type=str) +argsparser.add_argument("-p","--police",help="Herschey font to use",default="futural",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose + +text = args.text +fontname = args.police + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +# Useful variables init. +white = rgb2int((255,255,255)) +red = rgb2int((255,0,0)) +blue = rgb2int((0,0,255)) +green = rgb2int((0,255,0)) + +color = 65280 + +shape =[] + +Allfonts = ['futural', 'astrology', 'cursive', 'cyrilc_1', 'cyrillic', 'futuram', 'gothgbt', 'gothgrt', 'gothiceng', 'gothicger', 'gothicita', 'gothitt', 'greek', 'greekc', 'greeks', 'japanese', 'markers', 'mathlow', 'mathupp', 'meteorology', 'music', 'rowmand', 'rowmans', 'rowmant', 'scriptc', 'scripts', 'symbolic', 'timesg', 'timesi', 'timesib', 'timesr', 'timesrb'] + +thefont = HersheyFonts() +#thefont.load_default_font() +thefont.load_default_font(fontname) +thefont.normalize_rendering(120) + +for (x1, y1), (x2, y2) in thefont.lines_for_text(text): + shape.append([x1, -y1+400, color]) + shape.append([x2 ,-y2+400, color]) + +while True: + + start = time.time() + print(shape, flush=True); + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + +#[[14.285714285714286, 100.0, 14.285714285714286, 25.0, 65280], [64.28571428571429, 100.0, 64.28571428571429, 25.0, 65280], [14.285714285714286, 64.28571428571429, 64.28571428571429, 64.28571428571429, 65280], [89.28571428571428, 53.57142857142858, 132.14285714285714, 53.57142857142858, 65280], [132.14285714285714, 53.57142857142858, 132.14285714285714, 60.714285714285715, 65280], [132.14285714285714, 60.714285714285715, 128.57142857142856, 67.85714285714286, 65280], [128.57142857142856, 67.85714285714286, 125.0, 71.42857142857143, 65280], [125.0, 71.42857142857143, 117.85714285714286, 75.0, 65280], [117.85714285714286, 75.0, 107.14285714285714, 75.0, 65280], [107.14285714285714, 75.0, 100.0, 71.42857142857143, 65280], [100.0, 71.42857142857143, 92.85714285714286, 64.28571428571429, 65280], [92.85714285714286, 64.28571428571429, 89.28571428571428, 53.57142857142858, 65280], [89.28571428571428, 53.57142857142858, 89.28571428571428, 46.42857142857143, 65280], [89.28571428571428, 46.42857142857143, 92.85714285714286, 35.714285714285715, 65280], [92.85714285714286, 35.714285714285715, 100.0, 28.571428571428573, 65280], [100.0, 28.571428571428573, 107.14285714285714, 25.0, 65280], [107.14285714285714, 25.0, 117.85714285714286, 25.0, 65280], [117.85714285714286, 25.0, 125.0, 28.571428571428573, 65280], [125.0, 28.571428571428573, 132.14285714285714, 35.714285714285715, 65280]] + diff --git a/generators/trckr.py b/generators/trckr.py new file mode 100644 index 0000000..3c9bdec --- /dev/null +++ b/generators/trckr.py @@ -0,0 +1,175 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +A Face tracker +v0.1.0 + +Get all points fom redis /trckr/frame/WSclientID points + +LICENCE : CC + +by cocoa and Sam Neurohack + +''' + +from __future__ import print_function +import time +import argparse +import sys +import redis +import ast + +name="generator::trckr" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="Face tracking generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-i","--id",help="Trckr client ID",default="0",type=str) +argsparser.add_argument("-s","--server",help="redis server IP (127.0.0.1 by default)", type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +idclient = args.id + +if args.server: + redisIP = args.server +else: + redisIP = "127.0.0.1" + + + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + +color = 65280 + + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +# Useful variables init. +white = rgb2int((255,255,255)) +red = rgb2int((255,0,0)) +blue = rgb2int((0,0,255)) +green = rgb2int((0,255,0)) + +# +# Redis functions +# + +r = redis.StrictRedis(host=redisIP , port=6379, db=0) + +# read from redis key +def fromKey(keyname): + + return r.get(keyname) + +# Write to redis key +def toKey(keyname,keyvalue): + return r.set(keyname,keyvalue) + +# +# Trckr faces +# + +TrckrPts = [[159.39, 137.68], [155.12, 159.31], [155.56, 180.13], [159.81, 201.6], [170.48, 220.51], [187.46, 234.81], [208.4, 244.68], [229.46, 248.21], [246.44, 244.91], [259.69, 234.83], [270.95, 221.51], [278.54, 204.66], [283.53, 185.63], [286.27, 165.79], [284.72, 144.84], [280.06, 125.01], [274.35, 118.7], [260.71, 117.23], [249.52, 118.86], [182.04, 121.5], [193.63, 114.79], [210.24, 114.77], [222.35, 117.57], [190.6, 137.49], [203.59, 132.42], [214.75, 137.58], [203.04, 140.46], [203.32, 136.53], [272.45, 141.57], [263.33, 135.42], [250.31, 138.89], [262.15, 143.27], [261.99, 139.37], [235.82, 131.74], [221.87, 156.09], [213.66, 165.88], [219.28, 173.53], [236.3, 175.25], [249.02, 174.4], [254.22, 167.81], [248.83, 157.39], [237.94, 147.51], [227.01, 168.39], [245.68, 170.02], [204.94, 197.32], [217.56, 192.77], [228.27, 190.55], [234.66, 192.19], [240.47, 191.09], [247.96, 193.87], [254.52, 199.19], [249.35, 204.25], [242.74, 207.16], [233.2, 207.87], [222.13, 206.52], [212.44, 203.09], [220.34, 198.74], [233.31, 200.04], [244.0, 199.6], [244.27, 197.8], [233.81, 197.44], [220.88, 196.99], [239.57, 162.69], [196.52, 133.86], [210.2, 133.98], [209.43, 139.41], [196.59, 139.47], [268.99, 137.59], [256.36, 136.02], [255.95, 141.5], [267.9, 142.85]] +toKey('/trckr/frame/0',str(TrckrPts)) + +# get absolute face position points +def getPART(TrckrPts, pose_points): + + dots = [] + #debug(pose_points) + #debug(TrckrPts) + for dot in pose_points: + dots.append((TrckrPts[dot][0], TrckrPts[dot][1],0)) + #debug(dots) + return dots + + +# Face keypoints +def face(TrckrPts): + pose_points = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] + return getPART(TrckrPts, pose_points) + +def browL(TrckrPts): + pose_points = [15,16,17,18] + return getPART(TrckrPts, pose_points) + +def browR(TrckrPts): + pose_points = [22,21,20,19] + return getPART(TrckrPts, pose_points) + +def eyeR(TrckrPts): + pose_points = [25,64,24,63,23,66,26,65,25] + return getPART(TrckrPts, pose_points) + +def eyeL(TrckrPts): + pose_points = [28,67,29,68,30,69,31,28] + return getPART(TrckrPts, pose_points) + +def pupR(TrckrPts): + pose_points = [27] + return getPART(TrckrPts, pose_points) + +def pupL(TrckrPts): + pose_points = [32] + return getPART(TrckrPts, pose_points) + + +def nose1(TrckrPts): + pose_points = [62,41,33] + return getPART(TrckrPts, pose_points) + +def nose2(TrckrPts): + pose_points = [40,39,38,43,37,42,36,35,34] + return getPART(TrckrPts, pose_points) + +def mouth(TrckrPts): + pose_points = [50,49,48,47,46,45,44,55,54,53,52,51,50] + return getPART(TrckrPts, pose_points) + +def mouthfull(TrckrPts): + pose_points = [50,49,48,47,46,45,44,55,54,53,52,51,50,59,60,61,44,56,57,58,50] + return getPART(TrckrPts, pose_points) + + +while True: + + start = time.time() + shape =[] + points = ast.literal_eval(fromKey('/trckr/frame/'+idclient).decode('ascii')) + shape.append(browL(points)) + shape.append(eyeL(points)) + shape.append(browR(points)) + shape.append(eyeR(points)) + shape.append(pupL(points)) + shape.append(pupR(points)) + shape.append(nose1(points)) + shape.append(nose2(points)) + shape.append(mouthfull(points)) + line = str(shape) + line = line.replace("(",'[') + line = line.replace(")",']') + line = "[{}]".format(line) + print(line, flush=True); + #debug(shape) + #print(shape, flush=True); + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/generators/tunnel.py b/generators/tunnel.py new file mode 100755 index 0000000..437cb12 --- /dev/null +++ b/generators/tunnel.py @@ -0,0 +1,194 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + + +''' + +Woooh! I'm progressing in a tunnel ! +v0.1.0 + +Use it to test your filters and outputs + +LICENCE : CC + +by cocoa + +''' + +from __future__ import print_function +import argparse +import math +import random +import sys +import time +name="generator::tunnel" + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + + +argsparser = argparse.ArgumentParser(description="tunnel generator") +argsparser.add_argument("-c","--color",help="Color",default=65280,type=int) +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-i","--interval",help="point per shape interval",default=30,type=int) +argsparser.add_argument("-m","--max-size",help="maximum size for objects",default=400,type=int) +argsparser.add_argument("-r","--randomize",help="center randomization",default=5,type=int) +argsparser.add_argument("-s","--speed",help="point per frame progress",default=3,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-x","--centerX",help="geometrical center X position",default=400,type=int) +argsparser.add_argument("-y","--centerY",help="geometrical center Y position",default=400,type=int) + +args = argsparser.parse_args() +centerX = args.centerX +centerY = args.centerY +color = args.color +fps = args.fps +interval = args.interval +max_size = args.max_size +randomize = args.randomize +speed = args.speed +verbose = args.verbose + +origSpeed = speed +optimal_looptime = 1 / fps +square = [ + [-1,1], + [1,1], + [1,-1], + [-1,-1], + [-1,1] + ] + +circle = [[1,0], +[0.9238795325112867,0.3826834323650898], +[0.7071067811865476,0.7071067811865475], +[0.38268343236508984,0.9238795325112867], +[0,1.0], +[-0.3826834323650897,0.9238795325112867], +[-0.7071067811865475,0.7071067811865476], +[-0.9238795325112867,0.3826834323650899], +[-1.0,0], +[-0.9238795325112868,-0.38268343236508967], +[-0.7071067811865477,-0.7071067811865475], +[-0.38268343236509034,-0.9238795325112865], +[0,-1.0], +[0.38268343236509,-0.9238795325112866], +[0.707106781186548,-0.707106781186547], +[0.9238795325112872,-0.3826834323650887], +[1,0]] + +shape = circle +currentCenter = [centerX, centerY] +centerVector= [0,0] +# tweak random basis +if randomize % 2 == 1: + randomize += 1 +debug(name,"randomize:{}".format(randomize)) +centerRand = int(math.sqrt(randomize) / 4 ) + 1 +debug( name, "centerRand:{}".format(centerRand ) ) +class polylineGenerator( object ): + + def __init__( self ): + self.polylineList = [[0,[currentCenter[0],currentCenter[1]]]] + self.buf = [] + + def init(self): + finished = False + while not finished: + finished = self.increment() + debug(name,"init done:{}".format(self.polylineList)) + def draw( self ): + self.buf = [] + for it_pl, infoList in enumerate(self.polylineList): + size = infoList[0] + center = infoList[1] + for it_sqr, point in enumerate(shape): + x = int( center[0] + point[0]*size ) + y = int( center[1] + point[1]*size ) + # Add an invisible point in first location + if 0 == it_sqr: + self.buf.append([x,y,0]) + self.buf.append([x,y,color]) + #debug( name, "buf size:", str(len(self.buf)) ) + return self.buf + + def increment(self): + global speed + self.buffer = [] + min_size = 9999 + delList = [] + if randomize : + # Change the vector + centerVector[0] += random.randrange( -centerRand,centerRand ) + centerVector[1] += random.randrange( -centerRand,centerRand ) + # Modify the vector if it is over the limit + if currentCenter[0] + centerVector[0] >= centerX + randomize or currentCenter[0] + centerVector[0] <= centerX - randomize: + centerVector[0] = 0 + if currentCenter[1] + centerVector[1] >= centerY + randomize or currentCenter[1] +centerVector[1] <= centerY - randomize: + centerVector[1] = 0 + currentCenter[0] += centerVector[0] + currentCenter[1] += centerVector[1] + # Change speed + speed += int( random.randrange( int(-origSpeed),origSpeed ) ) + if speed < origSpeed : + speed = origSpeed + elif speed > (origSpeed + randomize / 2) : + speed = origSpeed + randomize / 2 + #debug(name, "currentCenter:{} speed:{}".format(currentCenter,speed)) + + for i, shapeInfo in enumerate(self.polylineList): + size = shapeInfo[0] + # Augment speed with size + """ + size = 0 : += sqrt(speed) + size = half max size : +=speed + + """ + if size < max_size / 4: + size += math.pow(speed, 0.1) + elif size < max_size / 3: + size += math.pow(speed, 0.25) + elif size < max_size / 2: + size += math.pow(speed, 0.5) + else: + size += math.pow(speed, 1.25) + if size < min_size : min_size = size + if size > max_size : delList.append(i) + self.polylineList[i][0] = size + for i in delList: + del self.polylineList[i] + #debug(name, "polyline:",self.polylineList) + if min_size >= interval: + debug(name, "new shape") + self.polylineList.append([0,[currentCenter[0],currentCenter[1]]]) + + # Return True if we delete a shape + + if len(delList): + return True + return False + + +pgen = polylineGenerator() +pgen.init() + +while True: + start = time.time() + + # Generate + pgen.increment() + + # send + pl = pgen.draw() + print(pl, flush=True) + #debug(name,"output:{}".format(pl)) + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + #debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + + diff --git a/generators/turtle.py b/generators/turtle.py new file mode 100644 index 0000000..9e26349 --- /dev/null +++ b/generators/turtle.py @@ -0,0 +1,657 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +Turtle library laser emulation +v0.1b + +by Sam Neurohack +from /team/laser + +""" + +from __future__ import print_function +import time +import argparse +import sys +import math + +from HersheyFonts import HersheyFonts + + +name="generator::turtle" + + +def debug(*args, **kwargs): + if( verbose == False ): + return + print(*args, file=sys.stderr, **kwargs) + +argsparser = argparse.ArgumentParser(description="Turtle graphics generator") +argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output") +argsparser.add_argument("-x","--LasCenterX",help="geometrical center X position",default=350,type=int) +argsparser.add_argument("-y","--LasCenterY",help="geometrical center Y position",default=350,type=int) +argsparser.add_argument("-p","--police",help="Herschey font to use",default="futural",type=str) +argsparser.add_argument("-m","--mode",help="Mode to use : ",default="clitools",type=str) +args = argsparser.parse_args() + +fps=args.fps +verbose=args.verbose +mode = args.mode +LasCenterX = args.LasCenterX +LasCenterY = args.LasCenterY +fontname = args.police + +Allfonts = ['futural', 'astrology', 'cursive', 'cyrilc_1', 'cyrillic', 'futuram', 'gothgbt', 'gothgrt', 'gothiceng', 'gothicger', 'gothicita', 'gothitt', 'greek', 'greekc', 'greeks', 'japanese', 'markers', 'mathlow', 'mathupp', 'meteorology', 'music', 'rowmand', 'rowmans', 'rowmant', 'scriptc', 'scripts', 'symbolic', 'timesg', 'timesi', 'timesib', 'timesr', 'timesrb'] + +thefont = HersheyFonts() +#thefont.load_default_font() +thefont.load_default_font(fontname) +thefont.normalize_rendering(120) + +CurrentColor = 255 +CurrentAngle = 0.0 +CurrentX = 0.0 +CurrentY = 0.0 + +shape = [] + + +optimal_looptime = 1 / fps +debug(name+" optimal looptime "+str(optimal_looptime)) + + +# +# Color functions +# + +# input hexcode = '0xff00ff' +def hex2rgb(hexcode): + + hexcode = hexcode[2:] + return tuple(int(hexcode[i:i+2], 16) for i in (0, 2, 4)) + #return tuple(map(ord,hexcode[1:].decode('hex'))) + +# input rgb=(255,0,255) output '0xff00ff' +def rgb2hex(rgb): + return '0x%02x%02x%02x' % tuple(rgb) + +#def rgb2hex(r, g, b): +# return hex((r << 16) + (g << 8) + b) + + +def rgb2int(rgb): + return int('0x%02x%02x%02x' % tuple(rgb),0) + +#def rgb2int(r,g,b): +# return int('0x%02x%02x%02x' % (r,g,b),0) + +def int2rgb(intcode): + #hexcode = '0x{0:06X}'.format(intcode) + hexcode = '{0:06X}'.format(intcode) + return tuple(int(hexcode[i:i+2], 16) for i in (0, 2, 4)) + +# +# Turtle +# + +def fd(distance): + forward(distance) +def forward(distance): + global CurrentX, CurrentY, shape + #Move the turtle forward by the specified distance, in the direction the turtle is headed. + + rad = CurrentAngle * math.pi / 180 + CurrentX = distance * math.cos(rad) + CurrentX + CurrentY = distance * math.sin(rad) + CurrentY + + shape.append([CurrentX + LasCenterX , CurrentY + LasCenterY , CurrentColor]) + + +def back(distance): + backward(distance) +def bk(distance): + backward(distance) +def backward(distance): + global CurrentX, CurrentY, shape + #Move the turtle backward by distance, opposite to the direction the turtle is headed. Do not change the turtle’s heading. + + rad = (CurrentAngle+180) * math.pi / 180 + CurrentX = distance * math.cos(rad) + CurrentX + CurrentY = distance * math.sin(rad) + CurrentY + + shape.append([CurrentX + LasCenterX, CurrentY + LasCenterY, CurrentColor]) + + +def right(angle): + rt(angle) +def rt(angle): + global CurrentAngle + #Turn turtle right by angle units. (Units are by default degrees, but can be set via the degrees() and radians() functions.) Angle orientation depends on the turtle mode, see mode(). + + CurrentAngle = CurrentAngle + angle + + +def left(angle): + lt(angle) +def lt(angle): + global CurrentAngle + #Turn turtle left by angle units. (Units are by default degrees, but can be set via the degrees() and radians() functions.) Angle orientation depends on the turtle mode, see mode(). + + CurrentAngle = CurrentAngle - angle + +def goto(x, y=None): + setposition(x, y=None) +def setpos(x, y=None): + setposition(x, y=None) +def setposition(x, y=None): + global CurrentX, CurrentY, shape + + #If y is None, x must be a pair of coordinates or a Vec2D (e.g. as returned by pos()). + # Move turtle to an absolute position. If the pen is down, draw line. Do not change the turtle’s orientation. + + CurrentX = x + CurrentY = y + shape.append([CurrentX + LasCenterX, CurrentY + LasCenterY, CurrentColor]) + + +def setx(x): + global CurrentX + #Set the turtle’s first coordinate to x, leave second coordinate unchanged. + CurrentX = x + + +def sety(y): + global CurrentY + #Set the turtle’s second coordinate to y, leave first coordinate unchanged. + CurrentY = y + + +def setheading(to_angle): + global CurrentAngle + #Parameters: to_angle – a number (integer or float) + CurrentAngle = to_angle + +def home(): + global CurrentX, CurrentY, CurrentAngle , shape + #Move turtle to the origin – coordinates (0,0) – and set its heading to its start-orientation (which depends on the mode, see mode()). + CurrentX = 0.0 + CurrentY = 0.0 + CurrentAngle = 0.0 + shape.append([CurrentX + LasCenterX, CurrentY + LasCenterY, CurrentColor]) + + +def circle(radius, extent=None, steps=None): + #Draw a circle with given radius. The center is radius units left of the turtle; extent – an angle – determines which part of the circle is drawn. If extent is not given, draw the entire circle. If extent is not a full circle, one endpoint of the arc is the current pen position. Draw the arc in counterclockwise direction if radius is positive, otherwise in clockwise direction. Finally the direction of the turtle is changed by the amount of extent. + print("Not yet in Laser turtle.") + ''' + >>> turtle.home() + >>> turtle.position() + (0.00,0.00) + >>> turtle.heading() + 0.0 + >>> turtle.circle(50) + >>> turtle.position() + (-0.00,0.00) + >>> turtle.heading() + 0.0 + >>> turtle.circle(120, 180) # draw a semicircle + >>> turtle.position() + (0.00,240.00) + >>> turtle.heading() + 180.0 + ''' + +def dot(size=None, *color): + #Draw a circular dot with diameter size, using color. If size is not given, the maximum of pensize+4 and 2*pensize is used. + print("Not yet in Laser turtle.") + + ''' + >>> turtle.home() + >>> turtle.dot() + >>> turtle.fd(50); turtle.dot(20, "blue"); turtle.fd(50) + >>> turtle.position() + (100.00,-0.00) + >>> turtle.heading() + 0.0 + ''' + +def stamp(): + + #Stamp a copy of the turtle shape onto the canvas at the current turtle position. Return a stamp_id for that stamp, which can be used to delete it by calling clearstamp(stamp_id). + print("Not yet in Laser turtle.") + ''' + >>> turtle.color("blue") + >>> turtle.stamp() + 11 + >>> turtle.fd(50) + ''' + +def clearstamp(stampid): + #Delete stamp with given stampid. + print("Not yet in Laser turtle.") + ''' + >>> turtle.position() + (150.00,-0.00) + >>> turtle.color("blue") + >>> astamp = turtle.stamp() + >>> turtle.fd(50) + >>> turtle.position() + (200.00,-0.00) + >>> turtle.clearstamp(astamp) + >>> turtle.position() + (200.00,-0.00) + ''' + +def clearstamps(n=None): + #Delete all or first/last n of turtle’s stamps. If n is None, delete all stamps, if n > 0 delete first n stamps, else if n < 0 delete last n stamps. + print("Not yet in Laser turtle.") + + ''' + >>> for i in range(8): + ... turtle.stamp(); turtle.fd(30) + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + >>> turtle.clearstamps(2) + >>> turtle.clearstamps(-2) + >>> turtle.clearstamps() + ''' + +def undo(): + #Undo (repeatedly) the last turtle action(s). Number of available undo actions is determined by the size of the undobuffer. + print("Not yet in Laser turtle.") + ''' + >>> for i in range(4): + ... turtle.fd(50); turtle.lt(80) + ... + >>> for i in range(8): + ... turtle.undo() + ''' + +def speed(speed=None): + #Set the turtle’s speed to an integer value in the range 0..10. If no argument is given, return current speed. + + print("Not yet in Laser turtle.") + + ''' + If input is a number greater than 10 or smaller than 0.5, speed is set to 0. Speedstrings are mapped to speedvalues as follows: + + “fastest”: 0 + “fast”: 10 + “normal”: 6 + “slow”: 3 + “slowest”: 1 + + Speeds from 1 to 10 enforce increasingly faster animation of line drawing and turtle turning. + + Attention: speed = 0 means that no animation takes place. forward/back makes turtle jump and likewise left/right make the turtle turn instantly. + >>> + + >>> turtle.speed() + 3 + >>> turtle.speed('normal') + >>> turtle.speed() + 6 + >>> turtle.speed(9) + >>> turtle.speed() + 9 + ''' + +def position(): + pos() +def pos(): + #Return the turtle’s current location (x,y) (as a Vec2D vector). + return (CurrentX,CurrentY) + +def towards(x, y=None): + #Return the angle between the line from turtle position to position specified by (x,y), the vector or the other turtle. This depends on the turtle’s start orientation which depends on the mode - “standard”/”world” or “logo”). + + # Currently only toward an x,y point + + xDiff = x - CurrentX + yDiff = y - CurrentY + return degrees(atan2(yDiff, xDiff)) + + +def xcor(): + #Return the turtle’s x coordinate. + return CurrentX + + +def ycor(): + #Return the turtle’s y coordinate. + return CurrentY + + +def heading(): + #Return the turtle’s current heading (value depends on the turtle mode, see mode()) + + return CurrentAngle + + +def distance(x, y=None): + #Return the distance from the turtle to (x,y), the given vector, or the given other turtle, in turtle step units. + + dist = math.sqrt((x - CurrentY)**2 + (y - CurrentY)**2) + return dist + + +def degrees(fullcircle=360.0): + #Set angle measurement units, i.e. set number of “degrees” for a full circle. Default value is 360 degrees. + print("Not yet in Laser turtle.") + ''' + >>> turtle.home() + >>> turtle.left(90) + >>> turtle.heading() + 90.0 + + Change angle measurement unit to grad (also known as gon, + grade, or gradian and equals 1/100-th of the right angle.) + >>> turtle.degrees(400.0) + >>> turtle.heading() + 100.0 + >>> turtle.degrees(360) + >>> turtle.heading() + 90.0 + ''' +def radians(): + #Set the angle measurement units to radians. Equivalent to degrees(2*math.pi). + print("Not yet in Laser turtle.") + + ''' + >>> turtle.home() + >>> turtle.left(90) + >>> turtle.heading() + 90.0 + >>> turtle.radians() + >>> turtle.heading() + 1.5707963267948966 + ''' + +def pendown(): + down() +def pd(): + down() +def down(): + global CurrentColor + #Pull the pen down – drawing when moving. + CurrentColor = Color + + +def penup(): + up() +def pu(): + up() +def up(): + #Pull the pen up – no drawing when moving. + global CurrentColor + + CurrentColor = 0 + + +def pensize(width=None): + width(width=None) +def width(width=None): + #Set the line thickness to width or return it. If resizemode is set to “auto” and turtleshape is a polygon, that polygon is drawn with the same line thickness. If no argument is given, the current pensize is returned. + print("Not yet in Laser turtle.") + ''' + >>> turtle.pensize() + 1 + >>> turtle.pensize(10) # from here on lines of width 10 are drawn + ''' + +def pen(pen=None, **pendict): + #Return or set the pen’s attributes in a “pen-dictionary” with the following key/value pairs: + + print("Not yet in Laser turtle.") + ''' + “shown”: True/False + “pendown”: True/False + “pencolor”: color-string or color-tuple + “fillcolor”: color-string or color-tuple + “pensize”: positive number + “speed”: number in range 0..10 + “resizemode”: “auto” or “user” or “noresize” + “stretchfactor”: (positive number, positive number) + “outline”: positive number + “tilt”: number + + This dictionary can be used as argument for a subsequent call to pen() to restore the former pen-state. Moreover one or more of these attributes can be provided as keyword-arguments. This can be used to set several pen attributes in one statement. + >>> + + >>> turtle.pen(fillcolor="black", pencolor="red", pensize=10) + >>> sorted(turtle.pen().items()) + [('fillcolor', 'black'), ('outline', 1), ('pencolor', 'red'), + ('pendown', True), ('pensize', 10), ('resizemode', 'noresize'), + ('shearfactor', 0.0), ('shown', True), ('speed', 9), + ('stretchfactor', (1.0, 1.0)), ('tilt', 0.0)] + >>> penstate=turtle.pen() + >>> turtle.color("yellow", "") + >>> turtle.penup() + >>> sorted(turtle.pen().items())[:3] + [('fillcolor', ''), ('outline', 1), ('pencolor', 'yellow')] + >>> turtle.pen(penstate, fillcolor="green") + >>> sorted(turtle.pen().items())[:3] + [('fillcolor', 'green'), ('outline', 1), ('pencolor', 'red')] + ''' + +def isdown(): + #Return True if pen is down, False if it’s up. + if CurrentColor != 0: + return True + else: + return False + + + +def pencolor(*args): + global CurrentColor + ''' + Return or set the pencolor. + + Four input formats are allowed: + + pencolor() + Return the current pencolor as color specification string or as a tuple (see example). May be used as input to another color/pencolor/fillcolor call. + pencolor(colorstring) + Set pencolor to colorstring, which is a Tk color specification string, such as "red", "yellow", or "#33cc8c". + pencolor((r, g, b)) + Set pencolor to the RGB color represented by the tuple of r, g, and b. Each of r, g, and b must be in the range 0..colormode, where colormode is either 1.0 or 255 (see colormode()). + pencolor(r, g, b) + + Set pencolor to the RGB color represented by r, g, and b. Each of r, g, and b must be in the range 0..colormode. + + If turtleshape is a polygon, the outline of that polygon is drawn with the newly set pencolor. + ''' + #print(args, len(args)) + if len(args) == 1: + colors = args[0] + #print(colors) + if colors[0]=="#": + CurrentColor = hex2int(colors) + else: + CurrentColor = rgb2int(colors) + print("CurrentColor:",CurrentColor) + + else: + print(int2rgb(CurrentColor)) + return int2rgb(CurrentColor) + ''' + >>> + + >>> colormode() + 1.0 + >>> turtle.pencolor() + 'red' + >>> turtle.pencolor("brown") + >>> turtle.pencolor() + 'brown' + >>> tup = (0.2, 0.8, 0.55) + >>> turtle.pencolor(tup) + >>> turtle.pencolor() + (0.2, 0.8, 0.5490196078431373) + >>> colormode(255) + >>> turtle.pencolor() + (51.0, 204.0, 140.0) + >>> turtle.pencolor('#32c18f') + >>> turtle.pencolor() + (50.0, 193.0, 143.0) + ''' +def fillcolor(*args): + # Return or set the fillcolor. + + print("Not yet in Laser turtle.") + + + +def color(*args): + global CurrentColor + #Return or set pencolor and fillcolor. + + if len(*args) ==2: + colors = args + if colors[0][0]=="#": + CurrentColor = hex2int(colors[0]) + + else: + print(int2rgb(CurrentColor),(0,0,0)) + return (int2rgb(CurrentColor),(0,0,0)) + + #rgb2int(rgb) + ''' + Several input formats are allowed. They use 0 to 3 arguments as follows: + + color() + Return the current pencolor and the current fillcolor as a pair of color specification strings or tuples as returned by pencolor() and fillcolor(). + color(colorstring), color((r,g,b)), color(r,g,b) + Inputs as in pencolor(), set both, fillcolor and pencolor, to the given value. + color(colorstring1, colorstring2), color((r1,g1,b1), (r2,g2,b2)) + + Equivalent to pencolor(colorstring1) and fillcolor(colorstring2) and analogously if the other input format is used. + + If turtleshape is a polygon, outline and interior of that polygon is drawn with the newly set colors. + + >>> + + >>> turtle.color("red", "green") + >>> turtle.color() + ('red', 'green') + >>> color("#285078", "#a0c8f0") + >>> color() + ((40.0, 80.0, 120.0), (160.0, 200.0, 240.0)) + + ''' + +def filling(): + # Return fillstate (True if filling, False else). + return False + +def begin_fill(): + + print("Not yet in Laser turtle.") + +def end_fill(): + #Fill the shape drawn after the last call to begin_fill(). + + print("Not yet in Laser turtle.") + + + +def reset(): + #Delete the turtle’s drawings from the screen, re-center the turtle and set variables to the default values. + global shape + + clear() + home() + +def clear(): + #Delete the turtle’s drawings from the screen. Do not move turtle. State and position of the turtle as well as drawings of other turtles are not affected. + global shape + shape = [] + + +def write(arg, move=False, align="left", font=("Arial", 8, "normal")): + global shape + ''' + Parameters: + + arg – object to be written to the TurtleScreen + move – True/False + align – one of the strings “left”, “center” or right” + font – a triple (fontname, fontsize, fonttype) + + Write text - the string representation of arg - at the current turtle position according to align (“left”, “center” or right”) and with the given font. If move is true, the pen is moved to the bottom-right corner of the text. By default, move is False. + ''' + for (x1, y1), (x2, y2) in thefont.lines_for_text(arg): + shape.append([x1, -y1+400, color]) + shape.append([x2 ,-y2+400, color]) + + ''' + >>> turtle.write("Home = ", True, align="center") + >>> turtle.write((0,0), True) + ''' + +def hideturtle(): + ht() +def ht(): + #Make the turtle invisible. It’s a good idea to do this while you’re in the middle of doing some complex drawing, because hiding the turtle speeds up the drawing observably. + print("Not yet in Laser turtle.") + + #>>> turtle.hideturtle() + +def showturtle(): + st() +def st(): + + #Make the turtle visible. + print("Not yet in Laser turtle.") + + + +def delay(delay=None): + + #Set or return the drawing delay in milliseconds. (This is approximately the time interval between two consecutive canvas updates.) The longer the drawing delay, the slower the animation. + print("Not yet in Laser turtle.") + ''' + Optional argument: + + >>> screen.delay() + 10 + >>> screen.delay(5) + >>> screen.delay() + 5 + ''' + + +def mainloop(): + done() +def done(): + #Starts event loop - calling Tkinter’s mainloop function. Must be the last statement in a turtle graphics program. Must not be used if a script is run from within IDLE in -n mode (No subprocess) - for interactive use of turtle graphics. + while True: + + start = time.time() + print(shape, flush=True); + + looptime = time.time() - start + if( looptime < optimal_looptime ): + time.sleep( optimal_looptime - looptime) + debug(name+" micro sleep:"+str( optimal_looptime - looptime)) + +def window_height(): + + #Return the height of the turtle window. + return LasCenterX * 2 + +def window_width(): + + #Return the width of the turtle window. + return LasCenterY*2 + + diff --git a/generators/turtle1.py b/generators/turtle1.py new file mode 100644 index 0000000..a5919b6 --- /dev/null +++ b/generators/turtle1.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' + +Example using experimental Laserized Turtle graphics library + +''' + +from turtle import * + +pencolor((255,0,0)) + +for i in range(4): + forward(100) + right(90) + +done() \ No newline at end of file diff --git a/runner.py b/runner.py new file mode 100755 index 0000000..ef27070 --- /dev/null +++ b/runner.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import runner_lib as runner + + + +def action_help(): + global bindings + print("\nKey\tAction\n--------------------------------------") + for i in bindings: + print(" {}\t{}".format(bindings[i],i)) + print("--------------------------------------\n") + + + +bindings={ + "Show playlist" : "l", + "Launch [0-x] cmd" : "0-x", + "Previous command" : "p", + "Next command" : "o", + "New command" : "a", + "Edit command" : "e", + "Delete command" : "d", + "Load playlist" : "L", + "Save playlist" : "S", + "Save as new" : "A", + "New playlist" : "N", + "Command help" : "H", + "Kill process Id" : "K", + "Edit Laser Id" : "i", + "Edit Laser Scene" : "s", + "Information" : "I", + "Help" : "h", + "Quit" : "q", + +} + + +## Init user contact + + + +# Main Loop +runner.action_info() +action_help() +print("\n\nLoad a playlist? [Y/n]: ") +if "y" == runner.inkey() : + runner.action_loadPlaylist() + +while True: + # Fuck zombies + runner._killBill() + runner._ok("> Next Action?") + k = runner.inkey() + + if bindings["Next command"] == k: + runner.action_changeCommand( 1 ) + runner.action_runCommand() + elif bindings["Previous command"] == k: + runner.action_changeCommand( -1 ) + runner.action_runCommand() + elif re.match( r'^\d+$',k): + runner.action_match(k) + runner.action_runCommand() + elif bindings["New command"] == k: + runner.action_newCommand() + continue + elif bindings["Show playlist"] == k: + runner.action_listAll() + continue + elif bindings["Delete command"] == k: + runner.action_deleteCommand() + continue + elif bindings["Edit command"] == k: + runner.action_listAll() + runner.action_edit() + continue + elif bindings["Load playlist"] == k: + if runner.action_loadPlaylist(): + runner.action_listAll() + continue + elif bindings["Save playlist"] == k: + runner.action_savePlaylist() + continue + elif bindings["Save as new"] == k: + runner.action_savePlaylist() + continue + elif bindings["New playlist"] == k: + runner.action_newPlaylist() + continue + elif bindings["Command help"] == k: + runner.action_commandHelp() + continue + elif bindings["Edit Laser Id"] == k: + runner.action_laserId() + continue + elif bindings["Edit Laser Scene"] == k: + runner.action_laserScene() + continue + elif bindings["Kill process Id"] == k: + runner.action_killPid() + continue + elif bindings["Help"] == k: + action_help() + continue + elif bindings["Information"] == k: + runner.action_info() + continue + elif bindings["Quit"] == k: + runner.action_quit() + else: + runner._err("Unexpected key:{}".format(k)) + continue + + + + diff --git a/runner_lib.py b/runner_lib.py new file mode 100644 index 0000000..85ddbe3 --- /dev/null +++ b/runner_lib.py @@ -0,0 +1,379 @@ + +import sys +import os +import signal +import subprocess +import time +import tty,termios +import re +import json +from pathlib import Path +import redis + + +environ = { +# "REDIS_IP" : "127.0.0.1", + "REDIS_IP" : "192.168.2.44", + "REDIS_PORT" : "6379", + "REDIS_KEY" : "/pl/0/0", + "REDIS_SCENE" : "0", + "REDIS_LASER" : "0" +} + +class bcolors: + HL = '\033[31m' + OKBLUE = '\033[94m' + OKGREEN = '\033[92m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +class _Getch: + def __call__(self): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch +inkey = _Getch() + +def intkey(): + try: + i = int( inkey() ) + return(i) + except ValueError: + print("Error.") + +current_id = 0 +current_cmd = "" +process = None +current_filename = "" +currentPlayList = [] +playlistsDir = Path("./playlists") +if not playlistsDir.is_dir() : playlistsDir.mkdir() + +def ask(q): + print(q) + return inkey() + +def _ok(msg): + print( bcolors.BOLD+bcolors.OKBLUE+ msg + bcolors.ENDC) + +def _err(msg): + print( bcolors.HL + msg + bcolors.ENDC) + +def _kill(process): + if process : + try: + pid = os.getpgid(process.pid) + os.killpg(pid, signal.SIGTERM) + os.killpg(pid, signal.SIGKILL) + os.kill(pid, signal.SIGTERM) + os.kill(pid, signal.SIGKILL) + process.terminate() + process.kill() + except Exception as e: + print("woops:{}".format(e)) + + + +def _killBill(): + subprocess.run("ps --ppid 1 -fo pid,sess,ppid,cmd | grep 'toRedis.py' | while read pid sid other; do pkill -9 -s $sid; done", shell=True,executable='/bin/bash') + + + +def action_info(): + print(""" +Welcome to LJ playlist manager + +Currently running on + +IP : {} +Port : {} +Key : {} +Scene : {} +Laser : {} +""".format( + environ["REDIS_IP"], + environ["REDIS_PORT"], + environ["REDIS_KEY"], + environ["REDIS_SCENE"], + environ["REDIS_LASER"] +)) + + +def action_changeCommand( inc ): + global currentPlayList + global current_id + if 0 == len(currentPlayList): + _err("Empty playlist") + return False + current_id = (current_id + 1) % len(currentPlayList) + return True + +def action_match( k ): + global current_id, currentPlayList + if int(k) > (len(currentPlayList) - 1): + print( bcolors.HL + "This key does not exist" + bcolors.ENDC ) + return False + else : + _ok("Changed action id to {}.".format(k)) + current_id = int(k) + +def action_runCommand(): + global currentPlayList + global current_id + global process + + # Get new command + try: + current_cmd = currentPlayList[current_id] + except IndexError as e: + _err("woops:{}".format(e)) + return False + + print("\n[!]New command:'{}'\n".format(current_cmd)) + + # Start subprocess + try : + _kill(process) + process = subprocess.Popen("./_run.sh '"+current_cmd+" | exports/toRedis.py -i $REDIS_IP -k $REDIS_KEY'", shell=True, executable='/bin/bash', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=environ, preexec_fn=os.setsid) + + except Exception as e: + print("woops:{}".format(e)) + + + +def action_newCommand(): + global currentPlayList + print("Enter new command or e(x)it.") + k = input() + # Exit early + if "x" == k: + return(False) + currentPlayList.append(k) + print(bcolors.OKBLUE + "Command added" + bcolors.ENDC) + return True + + + + +def action_deleteCommand(): + global currentPlayList + print("Select sequence to delete or e(x)it.") + action_listAll() + key = int(input()) + # Exit early + if "x" == key: + return(False) + del currentPlayList[key] + return True + + + +def action_listAll(): + global currentPlayList, current_cmd, current_id + print("\n--------------------------------------") + for i,seq in enumerate(currentPlayList): + pre="" + suf="" + if current_cmd == seq : + pre = bcolors.HL + suf = bcolors.ENDC + print( pre + "{}\t{}".format(i,seq) + suf ) + print("--------------------------------------\n") + + + + + + +def action_edit(): + print("Enter the command number to edit, or 'x' to abort.") + k = intkey() + if 'x' == k: + return + print("Enter the next value, or 'x' to abort.") + value = input() + if 'x' == value: + return + currentPlayList[k] = value + + + + + + + + +def action_loadPlaylist(): + + global playlistsDir + global currentPlayList + global current_playlist_name + # list files + i=0 + file_list = [x for x in playlistsDir.glob("*json")] + if 0 == len(file_list ): + print( bcolors.HL + "Error. No file in path '{}'\n".format(playlistsDir.name)) + return False + + print("\n Id\tName") + for k,name in enumerate(file_list) : + print(" {}\t{}".format(k,name),flush=True) + + # ask file + print("\nChoose a file or e(x)it:") + k = intkey() + if '' == k: + print("Invalid choice: '{}'".format(k)) + return + + # Exit early + if "x" == k: return(False) + + # todo : helper for detecting invalid keys + try: + if k > (len(file_list) - 1): + print( bcolors.HL + "This key '{}' does not exist".format(k) + bcolors.ENDC ) + return False + except TypeError: + print( bcolors.HL + "This key '{}' is not valid".format(k) + bcolors.ENDC ) + return False + + # @todo replace with _loadPlaylist + playlistFile = Path("./playlists/"+file_list[k].name) + currentPlayList = json.loads(playlistFile.read_text()) + current_playlist_name = file_list[k].name + current_id = 0 + print( bcolors.OKBLUE + "Playlist loaded: {}\n".format(current_playlist_name)+ bcolors.ENDC) + return True + + + +def _loadPlaylist( filename ): + + global currentPlayList, current_playlist_name, current_id + try: + playlistFile = Path(filename) + currentPlayList = json.loads(playlistFile.read_text()) + current_playlist_name = filename + current_id = 0 + _ok("Playlist loaded: {}\n".format(current_playlist_name)) + return True + except Exception as e: + _err("_loadPlaylist error when loading '{}':{}".format(filename,e)) + + + + + + +def action_newPlaylist(): + global playlistsDir + global currentPlayList + # ask for name + print("Enter new playlist name (without.json) or e(x)it question?") + k = input() + + # Exit early + if "x" == k: + return(False) + + # save file + currentPlayList = [] + _savePlaylist( k+".json" ) + currentPlayList = [] + + pass + + + + +def _savePlaylist( playlistname ): + global currentPlayList + filepath = Path("playlists/{}".format(playlistname)) + with filepath.open("w", encoding ="utf-8") as f: + f.write(json.dumps(currentPlayList, indent=4, sort_keys=True)) + return(True) + + + + + +def action_savePlaylist( name=False ): + global current_playlist_name + playlist_name = name if name else current_playlist_name + if not playlist_name : + _err("No name found.") + return False + try: + _savePlaylist(playlist_name) + print( bcolors.OKBLUE + "\nSaved as '{}'.\n".format(playlist_name) + bcolors.ENDC) + except Exception as e: + print("woops:{}".format(e)) + return False + + + + + +def action_commandHelp(): + global playlistsDir + + # iterate through files + file_list=[] + for folder in ["generators","filters","exports"]: + p = Path("./"+folder) + for plFile in Path("./"+folder).iterdir() : + if re.match("^.*py$",plFile.name): + file_list.append(os.path.join(folder,plFile.name)) + print("\n Id\tFile") + for k,filename in enumerate(file_list): + print(" {}\t{}".format(k,filename)) + print("\nChoose a file:") + k = int(input()) + print("\n-----------------------------------------------\n") + subprocess.run("python3 "+file_list[k]+" -h", shell=True, executable='/bin/bash') + print("\n-----------------------------------------------\n") + + + + +def _setKey( laser=0, scene=0 ): + global environ + laser = laser if laser else environ["REDIS_LASER"] + scene = scene if scene else environ["REDIS_SCENE"] + new_key = "/pl/{}/{}".format(scene,laser) + environ["REDIS_KEY"] = new_key + print("Sending new key '{}'".format(new_key)) + + +def action_laserId(): + k = int(ask("Enter the LJ Laser id [0-3]")) + _setKey( laser = k ) + + + +def action_laserScene(): + k = int(ask("Enter the LJ Scene id [0-3]")) + _setKey( scene = k ) + + + +def action_killPid(): + print("Enter pid to kill") + kill_pid = input() + subprocess.run("pkill -9 -s $(awk '{print $6}' /proc/$kill_pid/stat)", shell=True,executable='/bin/bash', env={"kill_pid":kill_pid}) + + +def action_quit(): + print("Quit [Y/n]?") + global process + quit = inkey() + if quit != "n": + _kill(process) + sys.exit(1) \ No newline at end of file diff --git a/runner_midi.py b/runner_midi.py new file mode 100755 index 0000000..979137b --- /dev/null +++ b/runner_midi.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 + +import argparse +import re +import redis +import runner_lib as runner +import time + +novationRows = [ + [ 0, 1, 2, 3, 4, 5, 6, 7 ], + [ *range(16,24)], + [ *range(32,40)], + [ *range(48,56)] +] + +argsparser = argparse.ArgumentParser(description="Playlist midi") +argsparser.add_argument("playlist",help="JSON playlist file ",type=str) +argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) +argsparser.add_argument("-r","--row",help="Row of Novation pad. Default:1 ",default=1,type=str) +argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str) +argsparser.add_argument("-l","--laser",help="Laser number. Default:0 ",default=0,type=int) +argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-s","--scene",help="Laser scene. Default:0 ",default=0,type=int) +argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") +args = argsparser.parse_args() + +ip = args.ip +port = args.port +key = args.key +verbose=args.verbose +laser = args.laser +scene = args.scene +playlist = args.playlist +row = args.row - 1 +rowKeys = novationRows[row] + + + +# Subscriber + +r = redis.StrictRedis(host=ip, port=port, db=0) +p = r.pubsub() +p.subscribe('/midi/last_event') +runner._killBill() + +# Set Laser and scene +runner._setKey( laser = laser, scene = scene) + +# Load playlist +runner._loadPlaylist( playlist ) + +print("Loaded playlist : {}".format(runner.currentPlayList)) + + +runner.action_info() +runner.current_id = -1 +while True: + runner._killBill() + + message = p.get_message() + if message: + #runner._ok ("Subscriber: %s" % message['data']) + + + # b'/midi/noteon/0/19/127' + match = re.match(".*/([0-9]+)/[0-9]+",str(message['data'])) + if not match: + continue + key = int(match.group(1)) + + # Check if the event is for us + if key not in rowKeys: + print("key {} not in {} ".format(key,rowKeys)) + continue + + try: + command_id = rowKeys.index(key) + cmd = runner.currentPlayList[command_id] + + if command_id != runner.current_id : + runner._ok("Launching command #{}\n Previous was {}\n Cmd:{}".format(command_id,runner.current_id,cmd)) + runner.action_match(command_id) + runner.action_runCommand() + else : + runner._err("Not running {} : already running.".format(command_id)) + except Exception as e : + print("Woops.",e)