thestk / rtmidi

A set of C++ classes that provide a common API for realtime MIDI input/output across Linux (ALSA & JACK), Macintosh OS X (CoreMIDI) and Windows (Multimedia)
Other
986 stars 274 forks source link

RtMidi can't communicate with its own virtual ports? #208

Open jholloway7 opened 5 years ago

jholloway7 commented 5 years ago

I apologize in advance for logging an issue, but couldn't figure out a better way to ask my question. I'm using mido which is a more idiomatic Python API on top of python-rtmidi which is, in turn, a simple binding on top of RtMidi. I'm developing on Mac OS X (Mojave) and the default Core MIDI driver.

The mido documentation has a peculiar note:

One oddity is that, at least in Linux, RtMidi can’t see its own virtual ports

I think maybe I'm running into the same thing in my environment.

I feel like my use case is a little abnormal, but "should work" and just looking for some guidance on why it's not.

I'm implementing a piece of software that acts as a virtual instrument driver. I need a MIDI clock that generates the right ticks for me to send notes, but I don't want to pigeonhole myself into an internal clock. I eventually want my program to play along to an external master clock -- imagine a DAW that has a beatmapped song where the tempo drifts.

In 'standalone' mode, my program is the master clock, but I also want it to be able to support a 'slave' mode.

I had this grand idea that I can just generalize things where my 'internal' clock sends MIDI clock events to a port just as I would expect them to come in from an external master clock. This would make my important code agnostic of the master clock -- whether it's internal or external.

I'm trying to open a virtual port that I can send/receive in the same process or even from a child process. I've used a MIDI monitor and I can see the output port opens and sends the message, but it's never received on the input port.

Am I missing something fundamental that prevents this type of 'loopback' message exchange to a port that's opened for both sending and receiving in the same process or a child process?

One thing I haven't tried yet is two totally disparate processes sending/receiving over a RtMidi virtual port.

At the end of the day, I don't really need this generalization because I can just implement things a slightly different way for my internal clock mode. I'm just scratching my head and hoping if I can understand the problem here I might learn something.

Thanks

jholloway7 commented 5 years ago

One thing I haven't tried yet is two totally disparate processes sending/receiving over a RtMidi virtual port.

I just tried this and it doesn't seem to work either. It's like you can't have a RtMidi program send MIDI events to another RtMidi program over a virtual port. Is that right?

SpotlightKid commented 5 years ago

I can't reproduce this on Linux (ALSA), maybe this is a OS X specific thing? Please provide minimal code to reproduce the problem.

Here's a Python interactive session where I send a MIDI note-on via a virtual output opened in the same process:

>>> from rtmidi.midiutil import open_midiinput, open_midioutput
>>> mo, _ = open_midioutput()
Do you want to create a virtual MIDI output port? (y/N) y
>>> mi, _ = open_midiinput()
Do you want to create a virtual MIDI input port? (y/N) 
Available MIDI ports:

[0] Midi Through:Midi Through Port-0 14:0
[1] Scarlett 6i6 USB:Scarlett 6i6 USB MIDI 1 20:0
[2] RtMidiOut Client:Virtual MIDI output 128:0

Select MIDI input port (Control-C to exit): 2
>>> def cb(ev, data):
...     print("%r %r" % ev)
... 
>>> mi.set_callback(cb)
>>> mo.send_message([0x90, 60, 127])
[144, 60, 127] 0.0

Also, please note that to receive MIDI clock, you need to call .ignore_types(timing=False) on your MidiIn instance:

https://spotlightkid.github.io/python-rtmidi/rtmidi.html#rtmidi.MidiIn.ignore_types

The same goes for C++ RtMidi.

SpotlightKid commented 5 years ago

Also works with Linux and JACK MIDI:

>>> from rtmidi.midiutil import open_midiinput, open_midioutput
>>> from rtmidi import API_UNIX_JACK
>>> mo, _ = open_midioutput(api=API_UNIX_JACK)
Do you want to create a virtual MIDI output port? (y/N) y
>>> mi, _ = open_midiinput(api=API_UNIX_JACK)
Do you want to create a virtual MIDI input port? (y/N) 
Available MIDI ports:

[0] system:midi_capture_1
[1] system:midi_capture_2
[2] RtMidiOut Client:Virtual MIDI output

Select MIDI input port (Control-C to exit): 2
>>> def cb(ev, data):
...     print("%r %r" % ev)
... 
>>> mi.set_callback(cb)
>>> mo.send_message([0x90, 60, 127])
[144, 60, 127] 0.0
SpotlightKid commented 5 years ago

And here's the same in the other direction, i.e. sending a MIDI note event to a virtual input port opened in the same process:

>>> from rtmidi.midiutil import open_midiinput, open_midioutput
>>> mi, _ = open_midiinput()
Do you want to create a virtual MIDI input port? (y/N) y
>>> def cb(ev, data):
...     print("%r %r" % ev)
... 
>>> mi.set_callback(cb)
>>> mo, _ = open_midioutput()
Do you want to create a virtual MIDI output port? (y/N) 
Available MIDI ports:

[0] Midi Through:Midi Through Port-0 14:0
[1] Scarlett 6i6 USB:Scarlett 6i6 USB MIDI 1 20:0
[2] RtMidiIn Client:Virtual MIDI input 129:0

Select MIDI output port (Control-C to exit): 2
>>> mo.send_message([0x90, 60, 127])
[144, 60, 127] 0.0
jholloway7 commented 5 years ago

Thanks for the response. It looks like the problem might be in trying to use_virtual=True on both sides. I guess I thought if both sender/receiver were software they would both have to open their end as virtual.

>>> from rtmidi.midiutil import open_midiinput, open_midioutput
>>> mo, _ = open_midioutput(use_virtual=True)
>>> mi, _ = open_midiinput(use_virtual=True)
>>> def cb(ev, data):
...      print("%r %r" % ev)
...
>>> mi.set_callback(cb)
>>> mo.send_message([0x90, 60, 127])
## no output ##
SpotlightKid commented 5 years ago

If you use virtual ports on both sides, you need to use external software to connect them. Otherwise use a virtual port on just one side and open it with open_port on the other side (which happens, if you don't choose "Do you want to create a virtual MIDI intput [or output] port?" when calling open_midiinput() resp. open_midioutput().

I suggest you look at the source of these helper functions to see what is going on exactly. They just wrap the functionality of MidiOut.get_ports(), `MidiOut.open_port() / .open_virtual_port() resp. MidIn.get_ports(), MidiIn.open_port / .open_virtual_port, to make it easier to write quick command line programs. For more complex port handling (e.g. in GUI programs) you should use those functions directly.

keinstein commented 5 years ago

Actually RtMidi is not designed to connect automatically to its own ports. It is designed to connect manually to everything. This is one thing that is solved in my fork (the mainline of the PortDescriptort patch) for doing automated backend testing on those backends that support virtual ports.

To illustrate the problem: As already said you need an input port and an output port, one of them virtual (in the language of RtMidi) and one of them a regular port. Suppose you are using the JACK backend and open a virtual input port with the name “RtMidi”. Then you can check all port names for “RtMidi” and connect to that port. Now you start a secound instance of that program. Its virtual port gets renamed by RtMidi to something like “RtMidi-1”, but the other end connects to “RtMidi” of the first instance, which can lead to undefined behaviour.

So you need to get the port number or port name from the RtMidi API which encodes the virtual port. I'm not sure whether this has been implemented, already (both need duplicating code from input ports to output ports and vice versa). Even if it is possible, you should always check the connection with a dedicated message (e.g. send your process id via SysEx) and a timeout, and reinitiate it if it doesn't work. The reason is that there is that the port number induces some race condition for some backends, that can result in the wrong connection. If you delete the normal API objeckt you (probably) can keep your virtual port.

fdrfaure commented 3 years ago

Hello, i have read your messages above, and it seems that i have a similar problem (question), that i don't understand yet, may be you can help me.

On Linux Alsa, in C++, i create a "virtual midi out port" called "MIDISYN", then i see two new midi ports: Pout: one port out called "RtMidi Input Client:RtMidi Input 133:0" Pin : and one port in called "RtMidi Output Client:MIDISYN 129:0" This is great for me, because in a thread T1, i send midi messages to the port out Pout, and in another thread T2, i receive these same midi messages on port in Pin that i convert to some sound. This is what i want to do.

Then i tried the same program on MacOs: if i create a virtual midi out port called "MIDISYN", then is see only one midi port in Pin called "MIDISYN" and i don't how to proceed with my two threads T1,T2 as above.

Do you have an idea or advice please?

keinstein commented 3 years ago

In alsa there is no difference between virtual ports, midii inputs and midi outputs. You must always create a port and connect it afterwards. RtMidi uses but does not support this feature. Actually it could hide the secound port but this is uncommon on linux systems. The MacOS implementation opens hidden ports when you connect to an existing port. This behaviour corresponds to the RtMidi philosophy: Either you create a virtual port or you connect to an existing port.

In conclusion: you found an easter egg in ALSA, but MacOS corresponds to RtMidi.

fdrfaure commented 3 years ago

Thanks for your explanations. Still i don't see how to proceed on MacOS, if i have two threads T1,T2 on the same computer: How is it possible to send midi messages from T1 to T2 via a virtual port, using RtMidi? (On Linux Alsa, if i create i virtual port Out, then it gives two ports Pin, Pout and then i can connect as follows T1 -> Pout ....> Pin -> T2 where the connection in dots is done by Alsa i suppose).

garyscavone commented 3 years ago

I’m not sure I understand the problem but you can establish a connection from one virtual output port to another virtual input port (or vice-versa).

On Nov 3, 2020, at 2:04 AM, Frédéric Faure notifications@github.com wrote:

Thanks for your explanations. Still i don't see how to proceed on MacOS, if i have two threads T1,T2 on the same computer: How is it possible to send midi messages from T1 to T2 via a virtual port, using RtMidi? (On Linux Alsa, if i create i virtual port Out, then it gives two ports Pin, Pout and then i can connect as follows T1 -> Pout ....> Pin -> T2 where the connection in dots is done by Alsa i suppose).

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/thestk/rtmidi/issues/208#issuecomment-720943167, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABJYDJJZF24YAP36NVMSDQ3SN6TRVANCNFSM4IKRW7HA.

[ { "@context": "http://schema.org", "@type": "EmailMessage", "potentialAction": { "@type": "ViewAction", "target": "https://github.com/thestk/rtmidi/issues/208#issuecomment-720943167", "url": "https://github.com/thestk/rtmidi/issues/208#issuecomment-720943167", "name": "View Issue" }, "description": "View this Issue on GitHub", "publisher": { "@type": "Organization", "name": "GitHub", "url": "https://github.com" } } ]

keinstein commented 3 years ago

If that did not help, please send a minimal working example, which explains what you are going to do with ALSA and why it does not work on MacOS.

another description of minimal working examples

a third descripton of MWEs

fdrfaure commented 3 years ago

Hello, thanks for your attention. I have now understood i think, the use of Virtual Midi ports with RtMidi. I understood and realized (if i am not wrong) that we can do the same things with a Virtual port input as with a Virtual port output, but their use is different, and the presentation of existing ports in Alsa and Mac is also different. Before, i was confused, because with Alsa there is a lot of other midi ports that appear when we open a (virtual or not) port.