Coder Social home page Coder Social logo

pymidi's Introduction

pymidi

A python RTP-MIDI / AppleMIDI implementation. You can use this library to build a network attached virtual MIDI device.

Build Status

Latest release: v0.5.0 (2020-01-12) (changelog)

Table of Contents

Quickstart

$ pip install pymidi

or

poetry install pymidi

See Using in Another Project and the Developer Setup wiki for more information.

Developer Setup

Set up your workspace with the very excellent Poetry:

$ poetry install

Once installed, you'll probably find it useful to work in a poetry shell, for ease of testing and running things:

$ poetry shell
(pymidi-tFFCbXNj)
$ python pymidi/server.py

Compatibility

pymidi requires Python 3. It has been tested against Python 3.6 and Python 3.7.

Running tests

Tests are run with pytest:

$ pytest

Developing against something else

If you're working on a project that uses pymidi and want to develop both concurrently, leverage the setuptools develop command:

$ cd ~/git/otherproject
$ poetry shell
$ pushd ~/git/pymidi && python setup.py develop && popd

This creates a link to ~/git/pymidi within the environment of ~/git/otherproject.

Demo Server and Examples

The library includes a simple demo server which prints stuff.

$ python pymidi/examples/example_server.py

See --help for usage. See the examples/ directory for other examples.

Using in Another Project

Most likely you will want to embed a server in another project, and respond to MIDI commands in some application specific way. The demo serve is an example of what you need to do.

First, create a subclass of server.Handler to implement your policy:

from pymidi import server

class MyHandler(server.Handler):
    def on_peer_connected(self, peer):
        print('Peer connected: {}'.format(peer))

    def on_peer_disconnected(self, peer):
        print('Peer disconnected: {}'.format(peer))

    def on_midi_commands(self, peer, command_list):
        for command in command_list:
            if command.command == 'note_on':
                key = command.params.key
                velocity = command.params.velocity
                print('Someone hit the key {} with velocity {}'.format(key, velocity))

Then install it in a server and start serving:

myServer = server.Server([('0.0.0.0', 5051)])
myServer.add_handler(MyHandler())
myServer.serve_forever()

See the Developer Setup wiki for ways to test with real devices.

Project Status

What works:

  • Exchange packet parsing
  • Timestamp sync packet parsing
  • Exchange & timestamp sync protocol support
  • MIDI message parsing

Not (yet) implemented:

  • Journal contents parsing
  • Verification of peers on the data channel
  • Auto-disconnect peers that stop synchronizing clocks

References and Reading

pymidi's People

Contributors

garthwebb avatar mik3y avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pymidi's Issues

README example does not work - bad syntax

There is example code in the README which does not work. Code should be more like this:

from pymidi import server

class MyHandler(server.Handler):
    def on_peer_connected(self, peer):
        print('Peer connected: {}'.format(peer))

    def on_peer_disconnected(self, peer):
        print('Peer disconnected: {}'.format(peer))

    def on_midi_commands(self, peer, command_list):
        for command in command_list:
            if command.command == 'note_on':
                key = command.params.key
                velocity = command.params.velocity
                print('Someone hit the key {} with velocity {}'.format(key, velocity))

myServer = server.Server({('0.0.0.0', 5051)})
myServer.add_handler(MyHandler())
myServer.serve_forever()

Images for Wiki

A place to drop images to use on the wiki, since oddly enough Github wiki doesn't have built-in image support.

Is the timestamp in handle_timestamp() incorrect?

I'm looking at line 156 of protocol.py:

now = int(time.time() * 10000)

I know that the standard says that this field is in increments of microseconds, etc. However:

The timestamp in the RTP header is only 32 bits. And the timestamps in each MIDI message that is sent are relative to the timestamps that are negotiated in the original "hello" phase of the session. I know that the RTP MIDI spec calls for 64 bit precision but that can't be right (except during the initial negotiation).

I've done some testing and bunch of packet captures between various different RTP MIDI handlers - so the timestamp might well be in microsecond increments but it has to be round down somehow. The Wikipedia article does say that:

this is not an absolute time, but a time related to a local reference

When I take out the * 10000 all of a sudden all my connections start working. So this is a good thing! ;-)

Anyway. The Linux 32 epoch is also coming up soon so I think that what is needed is to take the time, multiply by 10,000 then mod it by a specific (but constant) value. And this is going to be true everywhere in the code where the timestamp is put into the RTP header.

I'm happy to create a PR for this if you agree (I think it's kind of a big deal to be stumbling around inside the code this much).

Exception for _last_command_byte

Under some circumstances (it seems when there are many packets in flight from a remote peer) there is an exception raised:

Traceback (most recent call last):
  File "/home/ubuntu/python/construct/lib/containers.py", line 93, in __getattr__
    return self[name]
KeyError: '_last_command_byte'

I can see in the code where this is being used but I don't have enough knowledge to figure out how to solve this.

Server binding says "Address already in use"

cloned the code and tried server.py using default port and also port 2000 (as many others that are not in use).

Results:

$ python3 server.py --port 2000
INFO:pymidi.server:Data socket on 0.0.0.0:2001
INFO:pymidi.server:Control socket on 0.0.0.0:2000
INFO:pymidi.server:Data socket on :::2001
Traceback (most recent call last):
  File "server.py", line 157, in <module>
    server.serve_forever()
  File "server.py", line 108, in serve_forever
    self._init_protocols()
  File "server.py", line 100, in _init_protocols
    data_protocol = self._build_data_protocol(host, family)
  File "server.py", line 95, in _build_data_protocol
    data_socket.bind((host, self.port + 1))
OSError: [Errno 98] Address already in use

Ignoring unrecognized command: b'IN'

I get this every few seconds

WARNING:pymidi.ControlProtocol:Ignoring unrecognized command: b'IN'
WARNING:pymidi.ControlProtocol:Ignoring unrecognized command: b'BY'

Setting verbose mode crashes server:

Traceback (most recent call last):
  File "pymidi/server.py", line 157, in <module>
    server.serve_forever()
  File "pymidi/server.py", line 117, in serve_forever
    proto.handle_message(buffer, addr)
  File "/home/fakeusername/myvenv/lib/python3.6/site-packages/pymidi-0.3.0-py3.6.egg/pymidi/protocol.py", line 70, in handle_message
    self.logger.debug('rx: {}'.format(data.encode('hex')))
AttributeError: 'bytes' object has no attribute 'encode'

I'm connecting rtpMIDI tool by Tobias Erichsen from a Windows machine
also TouchDaw from an Android device. Both cause same errors.

No way to close client connection

Hey,
How does one close the socket?
Just closing it causes some program to hang.
I see some clients have some command 'BY' which might be it, but not sure how to use it

pymidi cannot handle names with apostrophes

If a computer name has an apostrophe in the invitation packet, pymidi will crash with a UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 6: ordinal not in range(128) in the handle_message() function.

Traceback (most recent call last):
  File "server.py", line 250, in <module>
    server.serve_forever()
  File "server.py", line 152, in serve_forever
    self._loop_once()
  File "server.py", line 147, in _loop_once
    proto.handle_message(buffer, addr)
  File "/usr/local/lib/python3.7/site-packages/pymidi/protocol.py", line 76, in handle_message
    self.handle_command_message(command, data, addr)
  File "/usr/local/lib/python3.7/site-packages/pymidi/protocol.py", line 87, in handle_command_message
    packet = packets.AppleMIDIExchangePacket.parse(data)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 304, in parse
    return self.parse_stream(io.BytesIO(data), **contextkw)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 316, in parse_stream
    return self._parsereport(stream, context, "(parsing)")
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 328, in _parsereport
    obj = self._parse(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 1979, in _parse
    subobj = sc._parsereport(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 328, in _parsereport
    obj = self._parse(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 2468, in _parse
    return self.subcon._parsereport(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 328, in _parsereport
    obj = self._parse(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 3480, in _parse
    obj = sc._parsereport(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 328, in _parsereport
    obj = self._parse(stream, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 715, in _parse
    return self._decode(obj, context, path)
  File "/usr/local/lib/python3.7/site-packages/construct/core.py", line 1490, in _decode
    return obj.decode(self.encoding)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 6: ordinal not in range(128)

For reference invitation packet appears as the following in wireshark
image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.