Skip to content

Instantly share code, notes, and snippets.

@RoarkGit
Last active April 9, 2024 18:01
Show Gist options
  • Select an option

  • Save RoarkGit/86c644faea50c79701e9ee3288aa44bc to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown

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
Copy Markdown

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
Copy Markdown
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