From 62c37a6e06346da3ccc5e72329cd09591f4667ea Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 22 Apr 2020 17:44:30 +0200 Subject: [PATCH] bugfixs --- OSC3.py | 2874 ------------------------------------------- client.py | 45 +- jamidi.json | 34 +- kick.wav | Bin 36230 -> 0 bytes main.py | 40 +- midi3.py | 562 --------- nozWS.py | 2 +- snare.wav | Bin 36240 -> 0 bytes websocket_server.py | 371 ------ 9 files changed, 101 insertions(+), 3827 deletions(-) delete mode 100755 OSC3.py delete mode 100755 kick.wav mode change 100644 => 100755 main.py delete mode 100644 midi3.py delete mode 100755 snare.wav delete mode 100755 websocket_server.py diff --git a/OSC3.py b/OSC3.py deleted file mode 100755 index 52a38f5..0000000 --- a/OSC3.py +++ /dev/null @@ -1,2874 +0,0 @@ -#!/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/client.py b/client.py index 9ac7dfe..1afca86 100644 --- a/client.py +++ b/client.py @@ -8,6 +8,20 @@ Jamidi Client v0.1b Input : local midi (hardware/software) instruments Output : Jamidi server + +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() +ws.send(message) +ws.send_message_to_all(msg = message) + +ws.close() + + ''' print("") @@ -25,11 +39,12 @@ import json from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) + +sys.path.append('libs/') import midi3 import types import websocket - try: import _thread except ImportError: @@ -123,7 +138,7 @@ def findMidiRules(rulename,ruletype): # Settings from jamidi.json # -# Load midi routing definitions in clientconfr.json +# Load midi definitions in jamidi.json def LoadConfs(): global Confs, nbmidiconf @@ -134,7 +149,7 @@ def LoadConfs(): Confs = json.loads(s) -# return midi confname number for given type 'Specials', 'cc2cc' +# return midi confname number for given type def findConfs(confname,conftype): #print("searching", midiconfname,'...') @@ -272,8 +287,9 @@ def on_message(ws, message): oscpath = message.split(" ") if debug > 0: - #print "Client got from WS", client['id'], "said :", message, "splitted in an oscpath :", oscpath - print("client got from WS :", message, "splitted in an oscpath :", oscpath) + print(GetTime(),"Main got from WS", client['id'], "said :", message, "splitted in an oscpath :", oscpath) + else: + print(GetTime(),"Main got WS Client", client['id'], "said :", message) wscommand = oscpath[0].split("/") @@ -286,6 +302,22 @@ def on_message(ws, message): args[0] = "noargs" #print "noargs command" + # CC : /device/cc/2 127 + elif wscommand[2] == "cc": + ccvr=int(wscommand[3]) #cc variable + ccvl=int(oscpath[1]) #cc value + if debug > 0: + print("ccvr=%d/ccvl=%d"%(ccvr,ccvl)) + if wscommand[1] == "ocs2": + #cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, Confs[wscommand[1]][0]["mididevice"]) + crtvalueOCS2[ccvr]=ccvl + else: + #cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, Confs[wscommand[1]][0]["mididevice"]) + crtvalueMMO3[ccvr]=ccvl + + cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, Confs[wscommand[1]][0]["mididevice"]) + + ''' # CC : /device/cc/2 127 elif wscommand[1] == "ocs2": if wscommand[2] == "cc": @@ -303,7 +335,7 @@ def on_message(ws, message): if wscommand[2] == "OSC1": print("Incoming MMO-3 WS OSC1", wscommand[3], ":", int(oscpath[1])) - + ''' # RESET : /device/reset 1 elif wscommand[2] == "reset": @@ -356,6 +388,7 @@ try: midi3.ws = ws midi3.clientmode = True + midi3.Confs = Confs print("Midi Configuration...") midi3.check() diff --git a/jamidi.json b/jamidi.json index b733c2c..dae3d52 100644 --- a/jamidi.json +++ b/jamidi.json @@ -34,7 +34,7 @@ "ocs2": [ { - "_comment": "OCS-2 parameters", + "_comment": "OCS-2 device parameters", "type": "mididevice", "mididevice": "UM-ONE:UM-ONE MIDI 1 20:0", "midichan" : 2 @@ -43,19 +43,47 @@ "mmo3": [ { - "_comment": "MMO-3 parameters", + "_comment": "MMO-3 device parameters", "type": "mididevice", "mididevice": "UM-ONE:UM-ONE MIDI 1 20:0", "midichan" : 1 } ], +"ocs2": [ + { + "_comment": "OCS-2 control with BCR2000", + "type": "mididevice", + "mididevice": "BCR2000 Port 1", + "midichan" : 2 + } +], + +"mmo3": [ + { + "_comment": "MMO-3 control with BCR2000", + "type": "mididevice", + "mididevice": "BCR2000 Port 1", + "midichan" : 1 + } + ], + +"launchpad": [ + { + "_comment": "Launchpad mini device parameters", + "type": "mididevice", + "mididevice": "Launchpad Mini", + "midichan" : 0 + } + ], + + "default": [ { "_comment": "Client : default midi device", "type": "mididevice", "mididevice": "BCR2000 Port 1", - "midichan" : 1 + "midichan" : 3 } ] diff --git a/kick.wav b/kick.wav deleted file mode 100755 index 80647fa1079c679600c4e210135e05f7bc9dabc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36230 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 diff --git a/main.py b/main.py old mode 100644 new mode 100755 index 223f83e..92ce80d --- a/main.py +++ b/main.py @@ -4,6 +4,17 @@ ''' Jamidi Server v0.1b +wserver = WebsocketServer(wsPORT,host=serverIP) + +wserver.set_fn_new_client(new_client) +wserver.set_fn_client_left(client_left) +wserver.set_fn_message_received(message_received) + +wserver.run_forever() + +wserver.send_message_to_all(message) + + ''' print("") @@ -18,6 +29,8 @@ import time from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) + +sys.path.append('libs/') import midi3 from websocket_server import WebsocketServer @@ -25,6 +38,7 @@ from websocket_server import WebsocketServer import types, json import argparse + debug = 1 @@ -66,6 +80,14 @@ else: print("Current values update at startup disabled") current = True +# Reset at startup ? +if args.reset == False: + print("Reset at startup disabled") + startreset = False +else: + print("Reset at startup enabled") + startreset = True + # reset = [64,64,0,32,96] # un truc comme ca pour les valeurs de reset ? resetMMO3 = [0] * 32 @@ -138,7 +160,7 @@ def sendallcurrentccvalues(nozoid): # Settings from jamidi.json # -# Load midi routing definitions in clientconfr.json +# Load midi definitions in jamidi.json def LoadConfs(): global Confs, nbmidiconf @@ -149,7 +171,7 @@ def LoadConfs(): Confs = json.loads(s) -# return midi confname number for given type 'Specials', 'cc2cc' +# return midi confname number for given type def findConfs(confname,conftype): #print("searching", midiconfname,'...') @@ -209,11 +231,8 @@ def message_received(client, wserver, message): else: print(GetTime(),"Main got WS Client", client['id'], "said :", message) - - # wscommand will be like ['', 'ocs2', 'cc', '9'] wscommand = oscpath[0].split("/") - # debug if debug > 0: print("wscommand :",wscommand) @@ -257,9 +276,6 @@ def message_received(client, wserver, message): elif wscommand[2] == "noteoff": midi3.NoteOff(int(oscpath[1]), Confs[wscommand[1]][0]["mididevice"]) - - - ws.send("/noteon "+str(msg[1])+" "+str(msg[2])) # Loop back : WS Client -> server -> WS Client sendWSall(message) @@ -293,8 +309,9 @@ try: # Websocket startup wserver = WebsocketServer(wsPORT,host=serverIP) midi3.ws = wserver + midi3.Confs = Confs + midi3.findJamDevice("UM-ONE:UM-ONE MIDI 1 20:0", 1) - #print wserver print("") print(GetTime(),"Launching Jamidi Websocket server...") print(GetTime(),"at", serverIP, "port",wsPORT) @@ -302,7 +319,8 @@ try: wserver.set_fn_client_left(client_left) wserver.set_fn_message_received(message_received) - if reset == True: + if startreset == True: + print("resetting nozoids...") reset("mmo3") reset("ocs2") @@ -312,6 +330,8 @@ try: wserver.run_forever() +except Exception: + traceback.print_exc() except KeyboardInterrupt: pass diff --git a/midi3.py b/midi3.py deleted file mode 100644 index 008c428..0000000 --- a/midi3.py +++ /dev/null @@ -1,562 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -""" -Midi3 light version for soundt/Jamidi/clapt -v0.7.0 - -Midi Handler : - -- Hook to the MIDI host -- Enumerate connected midi devices and spawn a process/device to handle incoming events - -by Sam Neurohack -from /team/laser - - -""" - - -import time -from threading import Thread - -import rtmidi -from rtmidi.midiutil import open_midiinput -from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, - PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) -import mido -from mido import MidiFile - -import traceback -import weakref -import sys -from sys import platform -import os - - -is_py2 = sys.version[0] == '2' -if is_py2: - from queue import Queue - from OSC import OSCServer, OSCClient, OSCMessage -else: - from queue import Queue - from OSC3 import OSCServer, OSCClient, OSCMessage - - -print("") - -midiname = ["Name"] * 16 -midiport = [rtmidi.MidiOut() for i in range(16) ] - -OutDevice = [] -InDevice = [] - -# max 16 midi port array - -midinputsname = ["Name"] * 16 -midinputsqueue = [Queue() for i in range(16) ] -midinputs = [] - -debug = 0 - -# False = server / True = Client -clientmode = False - -#Mser = False - -MidInsNumber = 0 - - -clock = mido.Message(type="clock") - -start = mido.Message(type ="start") -stop = mido.Message(type ="stop") -ccontinue = mido.Message(type ="continue") -reset = mido.Message(type ="reset") -songpos = mido.Message(type ="songpos") - -#mode = "maxwell" - -''' -print "clock",clock) -print "start",start) -print "continue", ccontinue) -print "reset",reset) -print "sonpos",songpos) -''' - -try: - input = raw_input -except NameError: - # Python 3 - Exception = Exception - - -STATUS_MAP = { - 'noteon': NOTE_ON, - 'noteoff': NOTE_OFF, - 'programchange': PROGRAM_CHANGE, - 'controllerchange': CONTROLLER_CHANGE, - 'pitchbend': PITCH_BEND, - 'polypressure': POLY_PRESSURE, - 'channelpressure': CHANNEL_PRESSURE -} - - - -notes = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"] -def midi2note(midinote): - - print("midinote",midinote, "note", notes[midinote%12]+str(round(midinote/12))) - return notes[midinote%12]+str(round(midinote/12)) - -# Send through websocket. -# Different websocket library for client (websocket) or server (websocket_server. -# ws object is added here by main.py or client.py startup : midi3.ws = -def wssend(message): - - if clientmode == True: - ws.send(message) - else: - ws.send_message_to_all(msg = message) - -# -# MIDI Startup and handling -# - -mqueue = Queue() -inqueue = Queue() - -# -# Events from Generic MIDI Handling -# - -def MidinProcess(inqueue, portname): - - inqueue_get = inqueue.get - - while True: - time.sleep(0.001) - msg = inqueue_get() - print("") - print("Generic from", portname,"msg : ", msg) - - # Note On - if msg[0]==NOTE_ON: - - MidiChannel = msg[0]-144 - MidiNote = msg[1] - MidiVel = msg[2] - print("NOTE ON :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) - #NoteOn(msg[1],msg[2],mididest) - wssend("/noteon "+str(msg[1])+" "+str(msg[2])) - - ''' - # Sampler mode : note <63 launch snare.wav / note > 62 kick.wav - if MidiNote < 63 and MidiVel >0: - - if platform == 'darwin': - os.system("afplay snare.wav") - else: - os.system("aplay snare.wav") - - - if MidiNote > 62 and MidiVel >0: - - if platform == 'darwin': - os.system("afplay kick.wav") - else: - os.system("aplay kick.wav") - ''' - - # Note Off - if msg[0]==NOTE_OFF: - print("NOTE OFF :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) - #NoteOff(msg[1],msg[2], mididest) - wssend("/noteoff "+str(msg[1])) - - - # MMO-3 Midi CC message CHANNEL 1 - if msg[0] == CONTROLLER_CHANGE: - print("channel 1 (MMO-3) CC :", msg[1], msg[2]) - print("Midi in process send /mmo3/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") - wssend("/mmo3/cc/"+str(msg[1])+" "+str(msg[2])) - - - # OCS-2 Midi CC message CHANNEL 2 - if msg[0] == CONTROLLER_CHANGE+1: - print("channel 2 (OCS-2) CC :", msg[1], msg[2]) - - print("Midi in process send /ocs2/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") - wssend("/ocs2/cc/"+str(msg[1])+" "+str(msg[2])) - - - - # other midi message - if msg[0] != NOTE_OFF and msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE: - pass - ''' - print("from", portname,"other midi message") - MidiMsg(msg[0],msg[1],msg[2],mididest) - ''' - - -def NoteOn(note,color, mididest): - global MidInsNumber - - - for port in range(MidInsNumber): - - # To mididest - if midiname[port].find(mididest) == 0: - midiport[port].send_message([NOTE_ON, note, color]) - - # To All - elif mididest == "all" and midiname[port].find(mididest) != 0 and midiname[port].find(BhorealMidiName) != 0 and midiname[port].find(LaunchMidiName) != 0: - midiport[port].send_message([NOTE_ON, note, color]) - - ''' - # To Launchpad, if present. - elif mididest == "launchpad" and midiname[port].find(LaunchMidiName) == 0: - launchpad.PadNoteOn(note%64,color) - - # To Bhoreal, if present. - elif mididest == "bhoreal" and midiname[port].find(BhorealMidiName) == 0: - gstt.BhorLeds[note%64]=color - midiport[port].send_message([NOTE_ON, note%64, color]) - #bhorosc.sendosc("/bhoreal", [note%64 , 0]) - ''' - - -def NoteOff(note, mididest): - global MidInsNumber - - - for port in range(MidInsNumber): - - # To mididest - if midiname[port].find(mididest) != -1: - midiport[port].send_message([NOTE_OFF, note, 0]) - - # To All - elif mididest == "all" and midiname[port].find(mididest) == -1 and midiname[port].find(BhorealMidiName) == -1 and midiname[port].find(LaunchMidiName) == -1: - midiport[port].send_message([NOTE_OFF, note, 0]) - - ''' - # To Launchpad, if present. - elif mididest == "launchpad" and midiname[port].find(LaunchMidiName) == 0: - launchpad.PadNoteOff(note%64) - - # To Bhoreal, if present. - elif mididest == "bhoreal" and midiname[port].find(BhorealMidiName) == 0: - midiport[port].send_message([NOTE_OFF, note%64, 0]) - gstt.BhorLeds[note%64] = 0 - #bhorosc.sendosc("/bhoreal", [note%64 , 0]) - ''' - - - -# Generic call back : new msg forwarded to queue -class AddQueue(object): - def __init__(self, portname, port): - self.portname = portname - self.port = port - #print "AddQueue", port) - self._wallclock = time.time() - - def __call__(self, event, data=None): - message, deltatime = event - self._wallclock += deltatime - #print "inqueue : [%s] @%0.6f %r" % ( self.portname, self._wallclock, message)) - message.append(deltatime) - midinputsqueue[self.port].put(message) - - -# -# MIDI OUT Handling -# - - -class OutObject(): - - _instances = set() - counter = 0 - - def __init__(self, name, kind, port): - - self.name = name - self.kind = kind - self.port = port - - self._instances.add(weakref.ref(self)) - OutObject.counter += 1 - - print("Adding OutDevice name", self.name, "kind", self.kind, "port", self.port) - - @classmethod - def getinstances(cls): - dead = set() - for ref in cls._instances: - obj = ref() - if obj is not None: - yield obj - else: - dead.add(ref) - cls._instances -= dead - - def __del__(self): - OutObject.counter -= 1 - - - -def OutConfig(): - global midiout, MidInsNumber - - # - if len(OutDevice) == 0: - print("") - print("MIDIout...") - print("List and attach to available devices on host with IN port :") - - # Display list of available midi IN devices on the host, create and start an OUT instance to talk to each of these Midi IN devices - midiout = rtmidi.MidiOut() - available_ports = midiout.get_ports() - - for port, name in enumerate(available_ports): - - midiname[port]=name - midiport[port].open_port(port) - #print ) - #print "New OutDevice [%i] %s" % (port, name)) - - OutDevice.append(OutObject(name, "generic", port)) - - #print "") - print(len(OutDevice), "Out devices") - #ListOutDevice() - MidInsNumber = len(OutDevice)+1 - -def ListOutDevice(): - - for item in OutObject.getinstances(): - - print(item.name) - -def FindOutDevice(name): - - port = -1 - for item in OutObject.getinstances(): - #print "searching", name, "in", item.name) - if name == item.name: - #print 'found port',item.port) - port = item.port - return port - - -def DelOutDevice(name): - - Outnumber = Findest(name) - print('deleting OutDevice', name) - - if Outnumber != -1: - print('found OutDevice', Outnumber) - delattr(OutObject, str(name)) - print("OutDevice", Outnumber,"was removed") - else: - print("OutDevice was not found") - - - -# -# MIDI IN Handling -# Create processing thread and queue for each device -# - -class InObject(): - - _instances = set() - counter = 0 - - def __init__(self, name, kind, port, rtmidi): - - self.name = name - self.kind = kind - self.port = port - self.rtmidi = rtmidi - self.queue = Queue() - - self._instances.add(weakref.ref(self)) - InObject.counter += 1 - - print("Adding InDevice name", self.name, "kind", self.kind, "port", self.port) - - @classmethod - def getinstances(cls): - dead = set() - for ref in cls._instances: - obj = ref() - if obj is not None: - yield obj - else: - dead.add(ref) - cls._instances -= dead - - def __del__(self): - InObject.counter -= 1 - - -def InConfig(): - - print("") - print("MIDIin...") - - # client mode - if debug > 0: - if clientmode == True: - print("midi3 in client mode") - else: - print("midi3 in server mode") - - print("List and attach to available devices on host with OUT port :") - - if platform == 'darwin': - mido.set_backend('mido.backends.rtmidi/MACOSX_CORE') - - genericnumber = 0 - - for port, name in enumerate(mido.get_input_names()): - - - outport = FindOutDevice(name) - midinputsname[port]=name - - #print "name",name, "Port",port, "Outport", outport) - # print "midinames", midiname) - - #ListInDevice() - - try: - #print name, name.find("RtMidi output")) - if name.find("RtMidi output") > -1: - print("No thread started for device", name) - else: - portin = object - port_name = "" - portin, port_name = open_midiinput(outport) - #midinputs.append(portin) - InDevice.append(InObject(name, "generic", outport, portin)) - - thread = Thread(target=MidinProcess, args=(midinputsqueue[port],port_name)) - thread.setDaemon(True) - thread.start() - - #print "Thread launched for midi port", port, "portname", port_name, "Inname", midiname.index(port_name) - #print "counter", InObject.counter - #midinputs[port].set_callback(AddQueue(name),midinputsqueue[port]) - #midinputs[port].set_callback(AddQueue(name)) - #genericnumber += 1 - InDevice[InObject.counter-1].rtmidi.set_callback(AddQueue(name,port)) - - except Exception: - traceback.print_exc() - - #print "") - print(InObject.counter, "In devices") - #ListInDevice() - - -def ListInDevice(): - - #print "known IN devices :" - for item in InObject.getinstances(): - - print(item.name) - print("") - -def FindInDevice(name): - - port = -1 - for item in InObject.getinstances(): - #print "searching", name, "in", item.name) - if name in item.name: - #print 'found port',item.port) - port = item.port - return port - - -def DelInDevice(name): - - Innumber = Findest(name) - print('deleting InDevice', name) - - if Innumber != -1: - print('found InDevice', Innumber) - delattr(InObject, str(name)) - print("InDevice", Innumber,"was removed") - else: - print("InDevice was not found") - - - -def End(): - global midiout - - #midiin.close_port() - midiout.close_port() - - #del virtual - if launchpad.Here != -1: - del launchpad.Here - if bhoreal.Here != -1: - del bhoreal.Here - if LPD8.Here != -1: - del LPD8.Here - -# mididest : all or specifiname, won't be sent to launchpad or Bhoreal. -def MidiMsg(midimsg, mididest): - - - desterror = -1 - - print("midi3 got midimsg", midimsg, "for", mididest) - - for port in range(len(OutDevice)): - # To mididest - if midiname[port].find(mididest) != -1: - if debug>0: - print("midi 3 sending to name", midiname[port], "port", port, ":", midimsg) - midiport[port].send_message(midimsg) - desterror = 0 - - if desterror == -1: - print("mididest",mididest, ": ** This midi destination doesn't exists **") - - # send midi msg over ws. - #if clientmode == True: - # ws.send("/ocs2/cc/1 2") - - - -def NoteOn(note, velocity, mididest): - global MidInsNumber - - - for port in range(MidInsNumber): - - # To mididest - if midiname[port].find(mididest) == 0: - midiport[port].send_message([NOTE_ON, note, velocity]) - - - -def listdevice(number): - - return midiname[number] - -def check(): - - OutConfig() - InConfig() - - - diff --git a/nozWS.py b/nozWS.py index 05f0b10..b2a49e0 100755 --- a/nozWS.py +++ b/nozWS.py @@ -424,7 +424,7 @@ try: on_close = on_close) midi3.ws = ws - midi3.wsmode = True + midi3.clientmode = True print("Midi Configuration...") print("Midi Destination", nozmidi) diff --git a/snare.wav b/snare.wav deleted file mode 100755 index c6a11429841d8e8210366692e6c3f99f475a6c3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36240 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%0APbtSEg%{#qT!ocUTG|KOt19p4 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 axr>-$Kg4Xup34~4kU~57dWbytrT+^k5a2xk diff --git a/websocket_server.py b/websocket_server.py deleted file mode 100755 index 9d1af5c..0000000 --- a/websocket_server.py +++ /dev/null @@ -1,371 +0,0 @@ -# Author: Johan Hanssen Seferidis -# License: MIT - -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) - - -# ------------------------- 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)