PortMidi / portmidi

portmidi is a cross-platform MIDI input/output library
Other
118 stars 33 forks source link

Allow subscription of created ALSA sequencer ports #51

Closed theGreatWhiteShark closed 1 year ago

theGreatWhiteShark commented 1 year ago

To support the same feature set on Linux as when using plain ALSA Midi, it would be great if the sequencer ports created by PortMidi could have the subscription capability. I think the way it's currently implemented - not allowing subscription - is the right default choice for most applications. But it would be nice to tweak this behavior.

As far as I understand the code this is exactly what *outputDriverInfo in Pm_OpenOutput and *inputDriverInfo in Pm_OpenInput is made for. However, neither alsa_in_open nor alsa_out_open makes any use of them.

How about defining a struct in portmidi.h representing additional ALSA driver infos and evaluating them within the functions mentioned above? Members of this struct could be further capability flags for the created port as well as its name (and probably a bunch of additional stuff I am not aware of).

I would do a PR but adding some content to portmidi.h feels way to bold.

rbdannenberg commented 1 year ago

I don't understand ALSA subscriptions. It looks like a subscription establishes a connection so that output to an output port is immediately inserted into an input port with no intervening receiving/sending process. Who owns the subscription and what is the lifecycle? Does macOS have similar capabilities? (I'm almost certain Windows does not, so this would be an additional non-portable "feature" for the portmidi API, but it already has virtual ports now, so it's no longer completely portable.)

theGreatWhiteShark commented 1 year ago

I don't understand ALSA subscriptions

Subscription exposes the created ports and allows other applications to connect to them. Per default only the application which created the ports is able to connect them with other ports (which support subscription).

Who owns the subscription and what is the lifecycle?

AFAIU lifecycle and ownership are the same as for ports without this capability. The only difference seems to be whether ALSA sequencer is listing them when asking for all available ports.

Does macOS have similar capabilities? (I'm almost certain Windows does not, so this would be an additional non-portable "feature" for the portmidi API, but it already has virtual ports now, so it's no longer completely portable.)

I have no idea about how things are handled in macOS.

But, yes, this a definitely a non-portable feature in order to fully support this particular driver. Isn't this the intended use of *input|outputDriverInfo? At least this is what I thought when reading the doc string.

a pointer to an optional driver-specific data structure containing additional information for device setup or handle processing. This parameter is never required for correct operation. If not used, specify NULL.

rbdannenberg commented 1 year ago

Thanks - ALSA should get you to write documentation! :-). But now I'm confused about what PortMidi already does vs. what "allow subscription" would do. In PortMidi, you can open virtual and real devices for input or output, and you can create virtual input or output devices that other applications can connect to. So what functionality is missing?

theGreatWhiteShark commented 1 year ago

In PortMidi, you can open virtual and real devices for input or output, and you can create virtual input or output devices that other applications can connect to. So what functionality is missing?

Ah. I wasn't aware of this. That might be the exact thing I was looking for. I will check

theGreatWhiteShark commented 1 year ago

In PortMidi, you can open virtual and real devices for input or output, and you can create virtual input or output devices that other applications can connect to. So what functionality is missing?

Okay. AFAICS in case I have an application using a single PmStream for MIDI input and another one for output, I have to decide whether to connect to an external port provided by another application or to create a virtual device and connect to it using its PmDeviceID, is it? In this case functionality missing is that ALSA allows to do both making a port discoverable and connecting it to another one. I don't see right now how to accomplish this in PortMidi.

A use-case is to let the application take care of a default connection but, at the same time, allow users to change routing using external tools, like QJackCtl or aconnect.

Another tiny but purely aesthetical piece missing is an interfaces to ALSA's snd_seq_set_client_name. Right now, virtual devices created with ALSA backend are added as named ports to clients called "Client-#ID". It would be more convenient to put the application's name in there.

rbdannenberg commented 1 year ago

For the snd_seq_set_client_name functionality, it looks like I'm adding a "manufacturer name" for macOS, so it would be consistent to support snd_seq_set_client_name. In this case, I think it will take two new functions: Pm_GetClientName(id) to get a name from an opened device (I'll have to look at how best to return a string), and Pm_SetClientName(char *name); to set a name for the whole program.

For the main question, you are correct and I think the situation is there because ALSA sees creating a port and connecting as separate operations, whereas PortMidi originally implemented the thing that all MIDI APIs support, which is opening a connection to a device (e.g. Windows MIDI output stream does not have an output port just like when you open a file for write, you do not create a file output port that is separately connected to a file.) Virtual ports are relatively new for PortMidi. If you connect a port to another one, why do you want it to be discoverable? I guess you are saying that the application might make a connection, and after that, some outside utility can come along and reconnect it to a different port?

I'll have to check but I suspect that to make PortMidi work in ALSA, it already creates a new port, but it's not discoverable. Maybe it's a simple matter of passing in a name when you open it, which is what we're doing for macOS and manufacturer names - so on ALSA, if you pass in a name as extra information, PortMidi would set the port name and make it discoverable.

If that much makes sense, the question remains: Can PortMidi make or change connections on an existing port (whether its a "normal" PortMidi opened device or a "virtual" PortMidi device? Changing connections might be something you can do in macOS too. I never intended to encourage operations that are not portable, but let me know what you think.

theGreatWhiteShark commented 1 year ago

If you connect a port to another one, why do you want it to be discoverable? I guess you are saying that the application might make a connection, and after that, some outside utility can come along and reconnect it to a different port?

Yes. This is the Linux-way of doing it. You open a large amount of tiny programs and use yet another program for connecting them. E.g. this picture shows the connection bay of QJackCtl with the pinkish stuff being MIDI input and output ports. There is even an ecosystem of session management programs allowing to set up and tear down such complex sessions. For most smaller applications the default is to open a port with subscription capability and do not even bother featuring a menu for connecting to a port within the program. Bigger applications, however, usually do and it feels strange if behavior between the two differs.

I'll have to check but I suspect that to make PortMidi work in ALSA, it already creates a new port, but it's not discoverable.

Yes.

Maybe it's a simple matter of passing in a name when you open it, which is what we're doing for macOS and manufacturer names - so on ALSA, if you pass in a name as extra information, PortMidi would set the port name and make it discoverable.

I just checked. The name of the port in ALSA is optional. What matters is the additional subscription capability.

diff --git a/pm_linux/pmlinuxalsa.c b/pm_linux/pmlinuxalsa.c
index 2a8efc1..a6e88e2 100755
--- a/pm_linux/pmlinuxalsa.c
+++ b/pm_linux/pmlinuxalsa.c
@@ -170,8 +171,10 @@ static PmError alsa_out_open(PmInternal *midi, void *driverInfo)
     if (!ainfo->is_virtual) {
         snd_seq_port_info_alloca(&pinfo);
         snd_seq_port_info_set_port(pinfo, id);
+       snd_seq_port_info_set_name(pinfo, "<OUTPUT_PORT_NAME>");^M
         snd_seq_port_info_set_capability(pinfo, SND_SEQ_PORT_CAP_WRITE |
-                                         SND_SEQ_PORT_CAP_READ);
+                                         SND_SEQ_PORT_CAP_READ |^M
+                                        SND_SEQ_PORT_CAP_SUBS_READ );^M
         snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC | 
                                    SND_SEQ_PORT_TYPE_APPLICATION);
         snd_seq_port_info_set_port_specified(pinfo, 1);
@@ -360,8 +363,10 @@ static PmError alsa_in_open(PmInternal *midi, void *driverInfo)
            number matches the portmidi device ID of the input device */
         snd_seq_port_info_alloca(&pinfo);
         snd_seq_port_info_set_port(pinfo, id);
+       snd_seq_port_info_set_name(pinfo, "<INPUT_PORT_NAME>");^M
         snd_seq_port_info_set_capability(pinfo, SND_SEQ_PORT_CAP_WRITE |
-                                         SND_SEQ_PORT_CAP_READ);
+                                         SND_SEQ_PORT_CAP_READ |^M
+                                        SND_SEQ_PORT_CAP_SUBS_WRITE );^M
         snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC | 
                                    SND_SEQ_PORT_TYPE_APPLICATION);
         snd_seq_port_info_set_port_specified(pinfo, 1);

Can PortMidi make or change connections on an existing port (whether its a "normal" PortMidi opened device or a "virtual" PortMidi device?

I'm not sure what you mean with changing connections. But establishing a connection using Pm_OpenOutput or Pm_OpenInput still works the same and creates a connection. Only difference is that this established connection can be altered later on by external tools.

rbdannenberg commented 1 year ago

Did you test the changes above? I tried a couple of variants but I can't get any port names to appear in the aconnectgui program I installed. Should that work? I pushed my latest attempts - it compiles and seems to run, and pm_test/{sendvirtual, testio, recvvirtual} programs all attempt to allow port naming, but that doesn't work. (See also some commented code that's closer to your code above in pmlinuxalsa.c - that didn't seem to work either.)

theGreatWhiteShark commented 1 year ago

Did you test the changes above? I tried a couple of variants but I can't get any port names to appear in the aconnectgui program I installed. Should that work?

Yes. I'm on a Debian 5.10.162-1 Linux with libasound2 1.2.4-1.1.

I did not use aconnectgui but using both QJackCtl and aconnect --list on the command line I do see the ports.

Using version 2.0.1 of PortMidi I already can create virtual devices which can be discovered using aconnect as well. Do you see their ports listed?

I pushed my latest attempts - it compiles and seems to run, and pm_test/{sendvirtual, testio, recvvirtual} programs all attempt to allow port naming, but that doesn't work. (See also some commented code that's closer to your code above in pmlinuxalsa.c - that didn't seem to work either.)

Thanks!

rbdannenberg commented 1 year ago

I made more changes. Thanks for the pointer to aconnect. Now, Linux ports are always created with writable subscription permission, so you can see and reconfigure connections externally (e.g. with aconnect). Aside from this, the PortMidi API is the same, and applications can still make their own connections. On Linux, by passing in extra parameters, you can name ports and/or set the client name. I was able to use pm_test/testio to prepare to sent to Midi Through, and in another shell use pm_test/testio to prepare to receive from Midi Through. Before starting, I deleted their connections and connected them directly using aconnect. Then, when I typed RETURN to send, the message was received by the other testio instance. And with new command line options, you can tell testio to give a name to the client and port it creates. Before too long, I want to do a full test on Linux/macOS/Windows and create a new release, but at least the HEAD seems to work with these new features (there's a set manufacturer ID option for macOS too).

theGreatWhiteShark commented 1 year ago

Thanks a lot!

I can confirm that both setting of device and port names as well as connecting using other applications works on 1f2b55705bb49e3ef9461c22ba69c2699d2f3339 .

FYI: I found https://github.com/gbevin/SendMIDI quite useful debugging/developing MIDI things. Especially for batch testing

rbdannenberg commented 1 year ago

I think we can close this now. Yes SendMIDI looks a lot more versatile than what is in pm_test/, although the pm_test programs are good for testing PortMidi specifically.

theGreatWhiteShark commented 1 year ago

Yes. Thank's again for taking care of this issue!