Last active
April 9, 2024 18:01
-
-
Save RoarkGit/86c644faea50c79701e9ee3288aa44bc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- WirePlumber script to handle inputs based on processing program state. This allows me to use | |
-- my virtual microphone in all programs and have it still work with or without REAPER open. | |
-- There is almost definitely a much more sane way of doing this, but I'm neither an audio | |
-- expert nor a Lua expert. | |
-- | |
-- The three nodes we care about are Scarlett (hardware microphone), REAPER (processor), and Voice (virtual microphone) | |
-- | |
-- When REAPER is open: | |
-- Scarlett->REAPER->Voice | |
-- | |
-- When REAPER is not open: | |
-- Scarlett->Voice | |
-- | |
-- Object Managers for the relevant nodes | |
-- REAPER | |
reaper_om = ObjectManager { | |
Interest { | |
type = 'node', | |
Constraint { 'node.name', 'equals', 'REAPER' }, | |
}, | |
} | |
-- Focusrite Scarlett Solo | |
scarlett_om = ObjectManager { | |
Interest { | |
type = 'node', | |
Constraint { 'node.name', 'equals', 'alsa_input.usb-Focusrite_Scarlett_Solo_USB_Y7YRMX017CBC1D-00.analog-stereo' }, | |
}, | |
} | |
scarlett_fr_om = ObjectManager { | |
Interest { | |
type = 'port', | |
Constraint { 'port.alias', 'equals', 'Scarlett Solo USB:capture_FR', }, | |
}, | |
} | |
-- Virtual microphone | |
voice_om = ObjectManager { | |
Interest { | |
type = 'node', | |
Constraint { 'node.name', 'equals', 'stream.voice' }, | |
}, | |
} | |
-- Desktop virtual output | |
desktop_om = ObjectManager { | |
Interest { | |
type = 'node', | |
Constraint { 'node.name', 'equals', 'stream.desktop.in' }, | |
}, | |
} | |
-- Link object manager | |
link_om = ObjectManager { | |
Interest { | |
type = 'link', | |
}, | |
} | |
-- Links table so they don't get garbage collected | |
links = {} | |
-- Port name : Port map | |
ports = {} | |
-- Port (OUT->IN) mappings for REAPER inactive/active | |
default_port_map = { | |
['Scarlett.capture_FL']={ 'Voice.playback_FL', 'Voice.playback_FR' }, | |
} | |
active_port_map = { | |
['Scarlett.capture_FL']={ 'REAPER.in1', 'REAPER.in2' }, | |
['REAPER.out1']={ 'Voice.playback_FL' }, | |
['REAPER.out2']={ 'Voice.playback_FR' }, | |
} | |
-- Connect signals | |
reaper_om:connect('object-added', function(om, node) | |
Log.message('REAPER connected') | |
establish_port('REAPER', node, true) | |
reset_links() | |
end) | |
scarlett_om:connect('object-added', function(om, node) | |
Log.message('Scarlett connected') | |
establish_port('Scarlett', node) | |
end) | |
voice_om:connect('object-added', function(om, node) | |
Log.message('Voice connected') | |
establish_port('Voice', node) | |
end) | |
-- Reset links when REAPER disconnects | |
reaper_om:connect('object-removed', function(om, node) | |
reset_links() | |
end) | |
-- Reset links to default state | |
function reset_links() | |
for pout, pins in pairs(default_port_map) do | |
for _, pin in ipairs(pins) do | |
if ports[pout] and ports[pin] then | |
if reaper_om:lookup() then | |
try_destroy_link(ports[pout], ports[pin]) | |
else | |
try_create_link(ports[pout], ports[pin]) | |
end | |
end | |
end | |
end | |
end | |
-- Establish port connection rules for a given node | |
function establish_port(prefix, node) | |
port_om = ObjectManager { | |
Interest { | |
type = 'port', | |
Constraint { 'node.id', 'equals', node.properties['object.id'] }, | |
}, | |
} | |
-- Create port ObjectManager for associated ports in order to make connections | |
port_om:connect('object-added', function(om, port) | |
local key = add_port_to_map(prefix, port) | |
local pin, pouts = get_ports(key) | |
if not pin or not pouts then return end | |
for _, pout in ipairs(pouts) do | |
try_create_link(pin, pout) | |
end | |
end) | |
port_om:activate() | |
end | |
-- Adds port to port map keyed by given prefix | |
function add_port_to_map(prefix, port) | |
ports[prefix .. '.' .. port.properties['port.name']] = port | |
return prefix .. '.' .. port.properties['port.name'] | |
end | |
-- Get ports associated with a key, return them in (in, out) order. | |
-- This could probably just be handled with port.direction instead. | |
function get_ports(key) | |
local port_map | |
-- Determine which port map to use based on REAPER active/inactive | |
if reaper_om:lookup() then | |
port_map = active_port_map | |
else | |
port_map = default_port_map | |
end | |
-- Key is on output side | |
if port_map[key] then | |
local ps = {} | |
for _, p in ipairs(port_map[key]) do | |
table.insert(ps, ports[p]) | |
end | |
return ports[key], ps | |
-- Key is on input side | |
else | |
for pin, pouts in pairs(port_map) do | |
for _, p in ipairs(pouts) do | |
if p == key then | |
return ports[pin], { ports[p] } | |
end | |
end | |
end | |
end | |
end | |
-- Create link from port_out->port_in | |
function try_create_link(port_out, port_in) | |
link = Link("link-factory", { | |
["link.output.port"] = port_out['bound-id'], | |
["link.input.port"] = port_in['bound-id'], | |
}) | |
link:activate(Features.ALL) | |
local key = tostring(port_out['bound-id']) .. '.' .. tostring(port_in['bound-id']) | |
links[key] = link | |
end | |
-- Destroy link from port_out->port_in | |
function try_destroy_link(port_out, port_in) | |
local key = tostring(port_out['bound-id']) .. '.' .. tostring(port_in['bound-id']) | |
local link = links[key] | |
if link then | |
link:request_destroy() | |
end | |
end | |
-- Automatically remove REAPER->Desktop virtual output and Scarlett Right-> connections | |
-- There is probably a better way to do this. | |
link_om:connect('object-added', function(om, link) | |
local reaper = reaper_om:lookup() | |
local desktop = desktop_om:lookup() | |
local scarlett = scarlett_fr_om:lookup() | |
if desktop and reaper then | |
if link.properties['link.output.node'] == tostring(reaper['bound-id']) and link.properties['link.input.node'] == tostring(desktop['bound-id']) then | |
link:request_destroy() | |
end | |
end | |
if scarlett then | |
if link.properties['link.output.port'] == tostring(scarlett['bound-id']) then | |
link:request_destroy() | |
end | |
end | |
end) | |
reaper_om:activate() | |
scarlett_om:activate() | |
scarlett_fr_om:activate() | |
voice_om:activate() | |
desktop_om:activate() | |
link_om:activate() |
I'm glad folks found this helpful. This is still basically what I'm using on Linux just with some different devices now. It's possible there is a more straightforward way to accomplish this sort of thing now but I actually have no idea. If anyone does find something simpler I would appreciate hearing about it, though!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I also wanted to thank you for posting this, I don't know why replicating what you can do with a few commands of "pw-link" is so difficult to translate to Wireplumber. I was able to modify it to my needs but you weren't joking about it being a "cursed" script, I really have no idea how this is working and where the documentation to do something like this was.