├── .gitignore ├── CHANGELOG.txt ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev-requirements.txt ├── docker └── Dockerfile ├── requirements.txt ├── setup.py ├── touchosc2midi ├── __init__.py ├── advertise.py ├── configuration.py └── touchosc2midi.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.0.1 ==> 0.0.2 2 | raise version string in order to fix broken pypi package 3 | 4 | 0.0.2 ==> 0.0.3 5 | added Dockerfile 6 | 7 | 0.0.3 ==> 0.0.4 8 | fix the option parsing for non-virtual midi ports 9 | 10 | 0.0.4 ==> 0.0.5 11 | fixed a bug with mido's in callback not getting initialized 12 | 13 | 0.0.5 ==> 0.0.6 14 | regression: last version introduced an indentation error, that lead to named midi-in not being initialized 15 | 16 | 0.0.6 ==> 0.0.7 17 | updated README and MANIFEST 18 | 19 | 0.0.7 ==> 0.0.8 20 | added fix for newer `zeroconf` and pinned `pyliblo<=0.10.0` 21 | 22 | 0.0.8 ==> 0.0.9 23 | fixed functionality of `--ip` argument 24 | 25 | 0.0.9 ==> 0.0.10 26 | added python 3 compatibility (thanks @SpotlightKid) 27 | added sysex support (touchOSC 1.9.4+) (thanks @SpotlightKid) 28 | changed network interface detection to a platform independent version (thanks @h3xl3r) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 velolala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include touchosc2midi *.py 2 | include requirements.txt 3 | include README.md 4 | include LICENSE 5 | include CHANGELOG.txt 6 | include tox.ini 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | touchosc2midi 2 | ============================================================== 3 | > a TouchOSC Bridge clone, aimed at linux, written in python. 4 | 5 | Motivation 6 | ---------- 7 | I wanted to have a TouchOSC Bridge running on a raspberrypi. After researching the options and running into several 8 | deadends, I figured out, I need to write my own. Specifically this program aims to achieve the following: 9 | 10 | - it works on linux 11 | - it works on ARM 12 | - it doesn't need the `.touchosc` layout-files 13 | - it can provide virtual midi ports, like the original TouchOSC Bridge from http://hexler.net/software/touchosc 14 | - it is open source 15 | - it advertises the service via `zeroconf` 16 | - it needs minimal configuration 17 | 18 | Dependencies 19 | ------------ 20 | `touchosc2midi` is built on top of these pip-installable packages: 21 | 22 | - `pyliblo` 23 | - `mido` (needs `python-rtmidi` and/or(FIXME!) `portmidi`) 24 | - `zeroconf` 25 | 26 | and without these, it wouldn't be such an embarrassingly trivial program. 27 | 28 | Installation 29 | ------------ 30 | 31 | ### Prerequisites 32 | You will need a recent version of `pip` and `cython` 33 | 34 | pip install -U pip 35 | pip install cython 36 | 37 | ### From pypi 38 | 39 | pip install touchosc2midi 40 | 41 | ### From source 42 | 43 | git clone https://github.com/velolala/touchosc2midi 44 | cd touchosc2midi 45 | pip install . 46 | 47 | `pyliblo` and `python-rtmidi` need some OS libraries installed (i.e. `liblo-dev` and `librtmidi-dev` Debian). Check out https://github.com/velolala/touchosc2midi/tree/master/docker/Dockerfile to see how to install from a plain Debian with python 2.7. 48 | 49 | Getting started 50 | --------------- 51 | After installation you should have a the `touchosc2midi` script in your path. Start it with 52 | 53 | touchosc2midi 54 | 55 | and open the "Midi Bridge" configuration dialog on your TouchOSC device. You should see an entry for your host. Click on your host and click "Done". Now you should have midi in- and out-ports named "TouchOSC Bridge" that you can use with your client software. 56 | 57 | Midi Configuration 58 | ------------------ 59 | This section shows you, how to do more specific midi configurations. 60 | 61 | ### Backends 62 | 63 | Since `touchosc2midi` uses `mido`, it can be configured with several backends (see: 64 | http://mido.readthedocs.org/en/latest/backends.html for details). 65 | 66 | By default it tries to mimic the behavior of the original `TouchOSC Bridge` (see: http://hexler.net/software/touchosc); that is: opening virtual in- and out-ports named "TouchOSC Bridge". Therefore, it tries to use an `rtmidi` backend by default, since only this backend allows the creation of virtual midi ports. 67 | 68 | Unfortunately, it get's more confusing, because `rtmidi` allows several API's (e.g. 'LINUX_ALSA', 'UNIX_JACK'). 69 | The default for `touchosc2midi` is to use the `rtmidi` backend with the first available/implemented API. 70 | 71 | If you want to change the backend, the command: 72 | 73 | touchosc2midi list backends 74 | 75 | lists the available full backend strings that you can use for the `MIDO_BACKEND=...` environment variable. 76 | To make use of another backend, call `touchosc2midi` like this: 77 | 78 | MIDO_BACKEND= touchosc2midi 79 | 80 | ### Midi Ports 81 | 82 | By default `touchosc2midi` uses virtual ports for midi-in and midi-out. You can, however, connect midi-ports directly. The command: 83 | 84 | touchosc2midi list ports 85 | 86 | lists all available ports with their ID and their port string. You can connect midi-in and midi-out ports either by ID or by their name string, e.g.: 87 | 88 | touchosc2midi --midi-in=1 --midi-out="iConnectMIDI4+ MIDI 11" 89 | 90 | Please note, that it is currently not possible to mix virtual and direct midi ports (but I'd be happy to accept your PR for this!). 91 | 92 | OSC Configuration 93 | ----------------- 94 | `touchosc2midi` tries to detect your main network interface for the network part automatically and you can expect this to work in most cases. You can, however, make it listen on a specific IP address: 95 | 96 | touchosc2midi --ip=192.168.0.53 97 | 98 | Docker 99 | ------ 100 | 101 | The git repository contains a `Dockerfile`. To use it: 102 | 103 | cd docker 104 | 105 | docker build -t touchosc2midi:latest . 106 | 107 | Above builds a container with all OS dependencies and `touchosc2midi` installed. When `run`ning, you will need to share the `/dev/snd/seq` device and expose the OSC receiving port, e.g. like this: 108 | 109 | docker run -p 0.0.0.0:12101:12101/udp --device=/dev/snd/seq:/dev/snd/seq touchosc2midi:latest 110 | 111 | Note, that when using docker, the `zeroconf` service announcement does not work, so you'll have to configure your ip address manually on the touchOSC device. 112 | 113 | 114 | License 115 | ------- 116 | This program is published under the MIT License. See `LICENSE` for details. 117 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | tox 2 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | RUN apt-get update &&\ 4 | apt-get install -y liblo-dev librtmidi-dev 5 | 6 | RUN pip install cython 7 | RUN pip install touchosc2midi 8 | 9 | ENTRYPOINT ["touchosc2midi"] 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/dsacre/pyliblo.git@33999ca8178a01c720e99856df769f1986c7e912#egg=pyliblo-0.10.0 2 | zeroconf==0.17.7 3 | python-rtmidi==1.0.0 4 | netifaces==0.10.5 5 | mido==1.1.24 6 | docopt==0.6.2 7 | Cython==0.25.2 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from touchosc2midi import __version__ 3 | 4 | requirement_dependency_link_replacements = { 5 | "git+https://github.com/dsacre/pyliblo.git@33999ca8178a01c720e99856df769f1986c7e912#egg=pyliblo-0.10.0": "pyliblo", 6 | } 7 | 8 | install_requires = list(set( 9 | requirement_dependency_link_replacements.get(requirement.strip(), requirement.strip()) 10 | for requirement in open('requirements.txt') if not requirement.lstrip().startswith('#') 11 | ) 12 | ) 13 | 14 | dependency_links = list(requirement_dependency_link_replacements.keys()) 15 | 16 | setup(name='touchosc2midi', 17 | version=__version__, 18 | description="TouchOSC Bridge clone in python", 19 | long_description=open("README.md").read(), 20 | author="velolala", 21 | author_email="fiets@einstueckheilewelt.de", 22 | url="https://github.com/velolala/touchosc2midi", 23 | license="LICENSE", 24 | install_requires=install_requires, 25 | dependency_links=dependency_links, 26 | packages=["touchosc2midi"], 27 | entry_points={"console_scripts": [ 28 | "touchosc2midi = touchosc2midi.touchosc2midi:main" 29 | ] 30 | }, 31 | classifiers=[ 32 | "Development Status :: 4 - Beta", 33 | "Environment :: Console", 34 | "License :: OSI Approved :: MIT License", 35 | "Operating System :: POSIX :: Linux", 36 | "Topic :: Artistic Software", 37 | "Topic :: Home Automation", 38 | "Topic :: Multimedia :: Sound/Audio :: MIDI", 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /touchosc2midi/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.11" 2 | -------------------------------------------------------------------------------- /touchosc2midi/advertise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Announce touchSCO-MIDI bridge via zeroconf. 4 | """ 5 | import socket 6 | import netifaces 7 | from time import sleep 8 | from zeroconf import Zeroconf, ServiceInfo 9 | import logging 10 | log = logging.getLogger(__name__) 11 | 12 | TOUCHOSC_BRIDGE = '_touchoscbridge._udp.local.' 13 | PORT = 12101 14 | 15 | 16 | def default_route_interface(): 17 | """ 18 | Query netifaces for the default route's ip. 19 | Note: this only checks for IPv4 addresses. 20 | """ 21 | interface = netifaces.gateways()['default'] 22 | if interface: 23 | name = interface[netifaces.AF_INET][1] 24 | ip = netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr'] 25 | log.debug("found '{}:{}' as default route.".format(name, ip)) 26 | return ip 27 | else: 28 | raise RuntimeError("default interface not found. Check your network or use --ip switch.") 29 | 30 | 31 | def build_service_info(ip): 32 | """Create a zeroconf ServiceInfo for touchoscbridge 33 | on for `ip` or the guessed default route interface's IP. 34 | """ 35 | return ServiceInfo(type_=TOUCHOSC_BRIDGE, 36 | name="{}.{}".format( 37 | socket.gethostname(), 38 | TOUCHOSC_BRIDGE 39 | ), 40 | address=socket.inet_aton(ip), 41 | port=PORT, 42 | properties=dict(), 43 | server=socket.gethostname() + '.local.') 44 | 45 | 46 | class Advertisement(object): 47 | def __init__(self, ip=None): 48 | """ 49 | :ip: if string `ip` given, register on given IP 50 | (if None: default route's IP). 51 | """ 52 | self.zeroconf = Zeroconf() 53 | self.info = build_service_info(ip=ip or default_route_interface()) 54 | 55 | def register(self): 56 | """Registers the service on the network. 57 | """ 58 | self.zeroconf.register_service(self.info) 59 | log.debug("Registered {} on {}:{}".format( 60 | self.info.name, 61 | self.ip, 62 | self.info.port 63 | )) 64 | 65 | def unregister(self): 66 | """Unregisters the service. 67 | """ 68 | self.zeroconf.unregister_service(self.info) 69 | log.debug("Unregistered touchoscbridge.") 70 | 71 | def update(self, ip=None): 72 | """Re-register the the service on the network. 73 | 74 | :ip: if string `ip` is given, use given IP when registering. 75 | """ 76 | self.unregister() 77 | self.info = build_service_info(ip=ip or default_route_interface()) 78 | self.register() 79 | 80 | def close(self): 81 | """Free resources. 82 | Advertisement.unregister() should be called before closing. 83 | """ 84 | self.zeroconf.close() 85 | 86 | def get_ip(self): 87 | """:return: the service's IP as a string. 88 | """ 89 | return socket.inet_ntoa(self.info.address) 90 | 91 | ip = property(get_ip) 92 | 93 | 94 | if __name__ == '__main__': 95 | logging.basicConfig(level=logging.DEBUG) 96 | service = Advertisement() 97 | try: 98 | service.register() 99 | while True: 100 | sleep(0.1) 101 | except KeyboardInterrupt: 102 | service.unregister() 103 | service.close() 104 | -------------------------------------------------------------------------------- /touchosc2midi/configuration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration helper for touchosc2midi. 3 | 4 | (c) 2015- velolala 5 | """ 6 | import os 7 | import mido 8 | import logging 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | VIRT_MIDI_PORT = "TouchOSC Bridge" 13 | 14 | 15 | class ConfigurationError(Exception): 16 | pass 17 | 18 | 19 | def get_rtmidi_backends(): 20 | """Return available full (backend + api) mido rtmidi configuration 21 | options as a list of strings. 22 | """ 23 | try: 24 | rtmidi = mido.Backend('mido.backends.rtmidi', load=True) 25 | return ['mido.backends.rtmidi/' + b for b in rtmidi.module.get_api_names()] 26 | except ImportError: 27 | return [] 28 | 29 | 30 | def get_mido_backend(): 31 | """Create the mido backend from environment. Note: if not configured 32 | in environment it will return the first available API for rtmidi. 33 | """ 34 | log.debug("MIDO_BACKEND from env: {}".format(os.environ.get('MIDO_BACKEND'))) 35 | if 'MIDO_BACKEND' not in os.environ: 36 | rtmidi_apis = get_rtmidi_backends() 37 | if rtmidi_apis: 38 | os.environ['MIDO_BACKEND'] = rtmidi_apis[0] 39 | if 'MIDO_BACKEND' in os.environ: 40 | backend = mido.Backend(os.environ.get('MIDO_BACKEND'), load=True) 41 | log.debug("Using backend {}.".format(backend)) 42 | return backend 43 | else: 44 | return mido 45 | 46 | 47 | def list_backends(): 48 | """Print the available mido backend configuration strings to stdout. 49 | """ 50 | log.info('Backends for environment variable MIDO_BACKEND=<...> :') 51 | for backend in get_rtmidi_backends() + ['mido.backends.portmidi']: 52 | log.info("\t{}".format(backend)) 53 | 54 | 55 | def list_ports(backend): 56 | """Print known midi in- and out-ports for the given `backend` to 57 | stdout. 58 | """ 59 | log.info("Midi in-ports:") 60 | for i, port in enumerate(backend.get_input_names()): 61 | log.info("\t{}: {}".format(i, port)) 62 | log.info("Midi out-ports:") 63 | for i, port in enumerate(backend.get_output_names()): 64 | log.info("\t{}: {}".format(i, port)) 65 | 66 | 67 | def configure_ioports(backend, virtual=True, mido_in=None, mido_out=None): 68 | """Create a midi in and a midi out port on mido backend `backend`. If 69 | virtual is True, try to create two virtual ports. 70 | """ 71 | log.debug("Backend for midi is {}.".format(backend)) 72 | midi_in = None 73 | midi_out = None 74 | if virtual: 75 | try: 76 | # we have to init with dummy callback, there seems to be a bug in mido 77 | midi_in = backend.open_input(VIRT_MIDI_PORT, virtual=True, callback=lambda x: x) 78 | midi_out = backend.open_output(VIRT_MIDI_PORT, virtual=True) 79 | except ImportError: 80 | log.error("Cannot open virtual IOports. Make sure, rtmidi is available" 81 | "or choose another backend.") 82 | raise ConfigurationError("Cannot open virtual IOports. Make sure, rtmidi" 83 | "is available or choose anoter backend") 84 | else: 85 | if mido_in and mido_in.isdigit(): 86 | mido_in = backend.get_input_names()[int(mido_in)] 87 | if mido_out and mido_out.isdigit(): 88 | mido_out = backend.get_output_names()[int(mido_out)] 89 | # we have to init with dummy callback, there seems to be a bug in mido 90 | midi_in = backend.open_input(mido_in, callback=lambda x: x) 91 | midi_out = backend.open_output(mido_out) 92 | log.debug("Inport is {}".format(midi_in)) 93 | log.debug("Outport is {}".format(midi_out)) 94 | return midi_in, midi_out 95 | -------------------------------------------------------------------------------- /touchosc2midi/touchosc2midi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | touchosc2midi -- a TouchOSC to Midi Bridge. 4 | 5 | Usage: 6 | touchosc2midi --help 7 | touchosc2midi list (backends | ports) [-v] 8 | touchosc2midi [(--midi-in --midi-out )] [--ip ] [-v] 9 | 10 | Options: 11 | -h, --help Show this screen. 12 | --midi-in= Full name or ID of midi input port. 13 | --midi-out= Full name of or ID midi output port. 14 | --ip= Network address for OSC server (default: guess). 15 | -v, --verbose Verbose output. 16 | """ 17 | 18 | from __future__ import absolute_import 19 | 20 | import logging 21 | import socket 22 | import time 23 | 24 | import liblo 25 | import mido 26 | 27 | from docopt import docopt 28 | 29 | from . import __version__ 30 | from .advertise import PORT, default_route_interface, Advertisement 31 | from .configuration import list_backends, list_ports, configure_ioports, get_mido_backend 32 | 33 | 34 | log = logging.getLogger(__name__) 35 | 36 | 37 | def message_from_oscsysexpayload(payload): 38 | """Convert the OSC string into a mido sysex message. 39 | """ 40 | payload = payload.lower() 41 | 42 | if len(payload) < 2: 43 | raise ValueError("sysex too short") 44 | elif not payload.startswith('f0'): 45 | raise ValueError("sysex doesn't start with F0") 46 | elif not payload.endswith('f7'): 47 | raise ValueError("sysex doesn't end with F7") 48 | 49 | sysex = tuple(int(payload[i:i + 2], 16) for i in range(0, len(payload), 2)) 50 | 51 | return mido.Message('sysex', data=sysex[1:-1]) 52 | 53 | 54 | def message_from_oscmidipayload(bites): 55 | """Convert the last 4 OSC-midi bytes into a mido message. 56 | """ 57 | bites = bites[::-1][0:4] 58 | return mido.parse(bites) 59 | 60 | 61 | def message_to_oscmidipayload(message): 62 | """OSC 1.0 specs: 'Bytes from MSB to LSB are: port id, status byte, data1, data2' 63 | However, touchOSC seems to be: port id, data2, data1, status byte 64 | """ 65 | # FIXME: port-id is 0? 66 | bites = [0] + message.bytes() 67 | assert len(bites) == 4 68 | return (bites[0], bites[3], bites[2], bites[1]) 69 | 70 | 71 | def message_to_oscsysexpayload(message): 72 | """Convert a sysex message into an OSC payload string. 73 | """ 74 | return message.hex().replace(' ', '') 75 | 76 | 77 | class OscHandler(object): 78 | def __init__(self, sink): 79 | self.sink = sink 80 | 81 | def on_osc(self, path, args, types, src): 82 | log.debug("OSC received from: {}:{} UDP: {} URL: {}".format( 83 | src.get_hostname(), 84 | src.get_port(), 85 | src.get_protocol() == liblo.UDP, 86 | src.get_url())) 87 | 88 | if path == '/midi' and types == 'm': 89 | log.debug("received /midi,m with arg {}".format(args[0])) 90 | msg = message_from_oscmidipayload(args[0]) 91 | elif path == '/sysex' and types == 's': 92 | log.debug("received /sysex,s with arg {}".format(args[0])) 93 | msg = message_from_oscsysexpayload(args[0]) 94 | 95 | log.debug("Sending MIDI message {}".format(msg)) 96 | self.sink.send(msg) 97 | 98 | 99 | class MidiHandler(object): 100 | def __init__(self, target): 101 | self.target = target 102 | 103 | def on_midi(self, message): 104 | if message.type == "clock": 105 | return 106 | 107 | log.debug("MIDI received: {}".format(message)) 108 | 109 | if message.type == "sysex": 110 | addr = '/sysex' 111 | arg = ('s', message_to_oscsysexpayload(message)) 112 | else: 113 | addr = '/midi' 114 | arg = ('m', message_to_oscmidipayload(message)) 115 | 116 | osc = liblo.Message(addr, arg) 117 | log.debug("Sending OSC {}, {} to: {}:{} UDP: {} URL: {}".format( 118 | addr, 119 | arg, 120 | self.target.get_hostname(), 121 | self.target.get_port(), 122 | self.target.get_protocol() == liblo.UDP, 123 | self.target.get_url())) 124 | liblo.send(self.target, osc) 125 | 126 | 127 | def wait_for_target_address(ip=None): 128 | """Waits for a byte on the OSC-PORT to arrive. 129 | Extract the sender IP-address, this will become our OSC target. 130 | """ 131 | log.info("Waiting for first package from touchOSC in order to setup target address...") 132 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 133 | s.bind((ip or default_route_interface(), PORT)) 134 | _, (address, _) = s.recvfrom(1) 135 | return address 136 | 137 | 138 | def main(): 139 | options = docopt(__doc__, version=__version__) 140 | logging.basicConfig(level=logging.DEBUG if options.get('--verbose') else logging.INFO, 141 | format="%(message)s") 142 | log.debug("Options from cmdline are {}".format(options)) 143 | backend = get_mido_backend() 144 | if options.get('list'): 145 | if options.get('backends'): 146 | list_backends() 147 | elif options.get('ports'): 148 | list_ports(backend) 149 | else: 150 | try: 151 | server = None 152 | midi_in, midi_out = configure_ioports( 153 | backend, 154 | virtual=not (options.get('--midi-in') or 155 | options.get('--midi-out')), 156 | mido_in=options.get('--midi-in'), 157 | mido_out=options.get('--midi-out') 158 | ) 159 | 160 | psa = Advertisement(ip=options.get('--ip')) 161 | psa.register() 162 | 163 | target_address = wait_for_target_address(psa.ip) 164 | 165 | log.debug("Listening for touchOSC on {}:{}.".format(psa.ip, PORT)) 166 | server = liblo.ServerThread(PORT) 167 | osc_handler = OscHandler(midi_out) 168 | server.add_method('/midi', 'm', osc_handler.on_osc) 169 | server.add_method('/sysex', 's', osc_handler.on_osc) 170 | 171 | target = liblo.Address(target_address, PORT + 1, liblo.UDP) 172 | log.info("Will send to {}.".format(target.get_url())) 173 | 174 | midi_handler = MidiHandler(target) 175 | midi_in.callback = midi_handler.on_midi 176 | 177 | log.info("Listening for midi at {}.".format(midi_in)) 178 | server.start() 179 | while True: 180 | time.sleep(.01) 181 | except KeyboardInterrupt: 182 | psa.unregister() 183 | psa.close() 184 | if server: 185 | server.stop() 186 | server.free() 187 | midi_in.close() 188 | log.info("closed all ports") 189 | 190 | 191 | if __name__ == '__main__': 192 | main() 193 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py34,py35 3 | 4 | [testenv] 5 | basepython = 6 | py27: python2.7 7 | py34: python3.4 8 | py35: python3.5 9 | deps = 10 | cython==0.25.2 11 | pytest 12 | flake8 13 | commands = 14 | flake8 touchosc2midi 15 | 16 | [flake8] 17 | max-line-length = 100 18 | --------------------------------------------------------------------------------