Skip to content

Instantly share code, notes, and snippets.

@RoarkGit
Last active April 9, 2024 18:01
Show Gist options
  • Save RoarkGit/86c644faea50c79701e9ee3288aa44bc to your computer and use it in GitHub Desktop.
Save RoarkGit/86c644faea50c79701e9ee3288aa44bc to your computer and use it in GitHub Desktop.
-- 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()
@Drayux
Copy link

Drayux commented Jul 18, 2023

I want to thank you so much for posting this. I'd been trying do something similar with my audio configuration on and off for over a year before I finally found this. I'm incredibly curious how you came to discover the link generation logic in try_create_link.

Further, I wanted to share that I've found that adding the flag ["object.linger"] = true to the properties when generating a link, saves me from all of the garbage collection workarounds.

@cody-ps
Copy link

cody-ps commented Aug 22, 2023

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.

@RoarkGit
Copy link
Author

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