Houston4444 / RaySession

Session manager for linux musical programs
GNU General Public License v2.0
177 stars 20 forks source link

feat: persistent/exclusive Patching #228

Open The-M1k3y opened 6 months ago

The-M1k3y commented 6 months ago

I want to use RaySession to manage a semi-complex patching on my desktop system. The problem is, that any new audio source spawned by a process always connects to the default audio sink of the system, in addition to the manually set connections.

Currently I work around this by managing the patching with qpwgraph. (Setting the default sink to a blackhole loopback is not an option.)

So I was wondering, if it's possible to implement something like qpwgraphs "Exclusive patchbay" mode. This mode automatically sets the patches to the saved connections and also disconnects all other connections (if there is a saved patch for that source/sink).

While I can get my wanted functionality with qpwgraph, it's UX is kind of crap and it doesn't support NSM (in exclusive mode it also overrides all patches set by other tools, which is kind of the purpose of it), so I would really like to see this feature availabale in the RaySession Patchbay.

Also I'm using pipewire as the audio backend. (Not sure if this is relevant)

Houston4444 commented 6 months ago

Hello !

Maybe the simplest way to do what you expect is to restrict self connect mode for applications using Pipewire through the JACK API.

If you take a look in this doc file : https://docs.pipewire.org/page_man_pipewire-jack_conf_5.html , If you set jack.self_connect_mode to 'fail-external', it should be ok.

The-M1k3y commented 6 months ago

Ah, I've already seen this option.

I think my request might have been worded badly. The actual behavior I want to achieve is the prevention of auto connections for nodes ONLY if there is a manually defined patch.

The basic idea behind the setup is, that I want to route various applications and other audio sources to specific ports on a carla patchbay to do some processing before they are send to the output.

But I still want all "random" sources to auto connect to the default sink (in my case a pipewire loopback device that acts as a "catch-all" fallback route).


I think there might be 2 ways to achieve this (but I'm neither a programmer nor an expert in pipewire):

  1. automatically disconnect all patches of a new node, if a patch has been defined for this node.
  2. prevent all auto-patching, but define a fallback patch if no specifically matching patch exists for this node.

I think option 2 might also allow for some rather great extension by allowing to define patch rules. This could be as simple as writing a set of expressions that try to match the new device and applying patches automatically. Regex support would be insanely awesome, as chrome for example tends to spawn a new audio source for every additional tab playing audio, suffixed by a somewhat random number, which is a behavior I don't think any patch management software is capable of handling at the moment.

Example (in yaml):

patchrules:

  - condition:                      # list of conditions that must match for this rule to execute. All conditions must be true. If no conditions are defined the rule always executes
      - label: __boxName            # automatic label
        matches: Spotify            # exact string match
    labels:
      - ApplicationType: Music      # set a custom user label for this execution of the ruler.
    patch:
      - sourcePortGroup: output     # the portgroup (of the new node) that will be patched. Can be omitted to patch the first port group of the box. Note that this can also be a playback portgroup if the new node is a sink.
        sourcePortName: FL          # the port that will be patched. Can be omitted for automatic stereo-stereo or similar patching
        targetBox: Carla            # patch to a specific box
        targetPortGroupName: in 2   # patch to specific portgroup on the box by name. Can be omitted to patch the first port group of the box
        targetPortName: L           # the port that will be patched to. Can be omitted for automatic stereo-stereo or similar patching
        patchMode: exclusive        # remove all other existing patches, regardless if they were set by another application or by RaySession itself
    ruleProcessing:
      - action: stop                # stop all rule processing for this box and apply all planned patches

  - condition:
      - label: __boxName
        matches: Amazon Music
    labels:
      - ApplicationType: Music

  - condition:
      - label: __boxName
        matchesRegex: Chrom.*       # matches the label with a regex
    labels:
      - ApplicationType: Browser

  - condition:
      - label: __boxName
        matches: Firefox
    labels:
      - ApplicationType: Browser

  - condition:
      - label: ApplicationType
        matches: Browser
    patch:
      - targetBox: Carla
        targetPortgroupName: in 4

  - condition:
      - label: ApplicationType
        matches: Music
      - label: __rulerPatchCount    # automatic label. Contains the number of patches that are planned at this point of the processing that were made by the ruler on the new box.
        matches: 0
    patch:
      - targetBox: Carla
        targetPortgroupIndex: 5     # patch to specific portgroup by index
        targetPortType: sink        # specify the type of portgroup to prevent ambiguity when addressing by index

  - condition:
      - label: __patchCount         # automatic label. Contains the number of patches that are planned at this point of the processing, including patches that already existed and were made by other applications.
        matches: 0
      - label: __nodeType
        matches: Application
      - label: __nodePorts          # automatic label. Contains a list of port types on the node. There is probably a better way to get this functionality
        matchesRegex: .*type=source.*
    patch:
      - targetBox: Carla
        targetPortgroupName: in 8

  - condition:
      - label: __rulerPatchCount
        matches: 0
      - label: nodeType
        matches: Device
    patch:                          # no patches defined but exclusive mode is set -> remove all connections and leave box unconnected
      - patchMode: exclusive

The syntax is inspired by the configuration of the prometheus monitoring software, specifically by the "scrape_config" and the included "relabel_config" sections. https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config

The idea would be to define a simple set of actions that can be applied based on rules. All rules are based on labels which are simple Key-Value pairs of strings. Labels prefixed with a double underscore are system labels that get set automatically.

The rules would be executed every time a new node appears in the system. They would only be executed once for the lifetime of a node and all (custom) labels only exist while the rule set gets evaluated and should be discarded afterwards. Conditions can only access information of the newly added node (by its labels) and the custom labels set by previous rules in this execution. Rules would be executed in order, potentially modifying the labels that are set and planning patches that should be made or removed. Once the rule set is processed, the planned patching (both disconnecting and connecting) would be applied.

I thought of a few options and automatic labels as an example that would match my use case, but there is probably a lot of potential for improvement.


Please let me know what your thoughts are on this (rather bold) proposal. I would also be happy to help implement such a feature, but my programming skills are at best those of a script kiddie. Although I'm happy to try if you can point me to the location in the code where this feature could be added best.


Edit: Maybe this is out of scope for RaySession and could be implemented as a standalone tool. Maybe you could point me at some resources on how to get started to write an application for this while using the NSM protocol?

Houston4444 commented 6 months ago

OK. I won't write a such thing, it seems to be very very long to write for a niche case, sorry.

What I can do quite easily is an option for ray-jackpatch to disconnect all not remembered connections at startup (startup of ray-jackpatch). ray-jackpatch already disconnects theses connections in case of session switch (opening a session without close the previous one).

I think that the best thing you could do would be to try to write your own ray-jackpatch (with a different name of course), which would still be a NSM client, and would have to be run in your sessions instead ray-jackpatch. For this:

The-M1k3y commented 6 months ago

I won't write a such thing, it seems to be very very long to write for a niche case, sorry.

Fair enough. The idea for the automatic ruler just came to me today, and I thought it could be a nice feature. I thought it won't hurt to ask :)

What I can do quite easily is an option for ray-jackpatch to disconnect all not remembered connections at startup (startup of ray-jackpatch).

Sounds like a sensible feature to have, although it won't help for my use case at all.

I think that the best thing you could do would be to try to write your own ray-jackpatch (with a different name of course), which would still be a NSM client, and would have to be run in your sessions instead ray-jackpatch. [...]

Thank you a lot for pointing my at a sensible starting point. I'll see when I get to it and how difficult it will be. I anticipate the biggest challenge to figure out the interfacing with JACK. The ruler itself should be quite simple actually, as I already purposely limited the capabilities in my first draft.

I'll let you know how it's going if you are interested. Just send me a mail to the address in my profile if you want to stay in contact.

Houston4444 commented 6 months ago

I anticipate the biggest challenge to figure out the interfacing with JACK.

Not so much if you start from ray-jackpatch, I think that all what you need from Jack is already done here. I forgot to mention that the only file you have to modify is main_loop.py, you can add files of course, but do not modify contents of the other files. You will have to make a git submodule for jacklib, linking to my jacklib fork (as RaySession does).

I'll let you know how it's going if you are interested. Just send me a mail to the address in my profile if you want to stay in contact.

Ok, I'll send you a mail, feel free to ask if you feel lose in the code.