alsa.lua 10.4 KB
Newer Older
1
2
3
4
5
6
7
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

8
-- Receive script arguments from config.lua
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
local config = ...

-- ensure config.properties is not nil
config.properties = config.properties or {}

-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
  r.interests = {}
  for _, i in ipairs(r.matches) do
    local interest_desc = { type = "properties" }
    for _, c in ipairs(i) do
      c.type = "pw"
      table.insert(interest_desc, Constraint(c))
    end
    local interest = Interest(interest_desc)
    table.insert(r.interests, interest)
  end
  r.matches = nil
27
28
end

29
30
31
32
33
34
35
36
37
38
39
40
41
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
  for _, r in ipairs(config.rules or {}) do
    if r.apply_properties then
      for _, interest in ipairs(r.interests) do
        if interest:matches(properties) then
          for k, v in pairs(r.apply_properties) do
            properties[k] = v
          end
        end
      end
    end
  end
42
43
end

44
45
46
47
48
49
50
51
function findDuplicate(parent, id, property, value)
  for i = 0, id - 1, 1 do
    local obj = parent:get_managed_object(i)
    if obj and obj.properties[property] == value then
      return true
    end
  end
  return false
52
53
end

54
55
56
57
function nonempty(str)
  return str ~= "" and str or nil
end

58
59
function createNode(parent, id, type, factory, properties)
  local dev_props = parent.properties
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

  -- set the device id and spa factory name; REQUIRED, do not change
  properties["device.id"] = parent["bound-id"]
  properties["factory.name"] = factory

  -- set the default pause-on-idle setting
  properties["node.pause-on-idle"] = false

  -- try to negotiate the max ammount of channels
  if dev_props["api.alsa.use-acp"] ~= "true" then
    properties["audio.channels"] = properties["audio.channels"] or "64"
  end

  local dev = properties["api.alsa.pcm.device"]
              or properties["alsa.device"] or "0"
  local subdev = properties["api.alsa.pcm.subdevice"]
                 or properties["alsa.subdevice"] or "0"
77
  local stream = properties["api.alsa.pcm.stream"] or "unknown"
78
79
  local profile = properties["device.profile.name"]
                  or (stream .. "." .. dev .. "." .. subdev)
80
81
  local profile_desc = properties["device.profile.description"]

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  -- set priority
  if not properties["priority.driver"] then
    local priority = (dev == "0") and 1000 or 744
    if stream == "capture" then
      priority = priority + 1000
    end

    priority = priority - (tonumber(dev) * 16) - tonumber(subdev)

    if profile:find("^analog%-") then
      priority = priority + 9
    elseif profile:find("^iec958%-") then
      priority = priority + 8
    end

    properties["priority.driver"] = priority
    properties["priority.session"] = priority
  end

101
102
103
104
105
106
107
108
109
110
  -- ensure the node has a media class
  if not properties["media.class"] then
    if stream == "capture" then
      properties["media.class"] = "Audio/Source"
    else
      properties["media.class"] = "Audio/Sink"
    end
  end

  -- ensure the node has a name
111
112
113
114
115
116
117
118
119
120
  if not properties["node.name"] then
    local name =
        (stream == "capture" and "alsa_input" or "alsa_output")
        .. "." ..
        (dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
         dev_props["device.name"] or
         "unnamed-device")
         .. "." ..
         profile

121
122
123
    -- sanitize name
    name = name:gsub("([^%w_%-%.])", "_")

124
125
126
127
128
129
130
131
132
133
134
135
136
    properties["node.name"] = name

    -- deduplicate nodes with the same name
    for counter = 2, 99, 1 do
      if findDuplicate(parent, id, "node.name", properties["node.name"]) then
        properties["node.name"] = name .. "." .. counter
      else
        break
      end
    end
  end

  -- and a nick
137
  local nick = properties["node.nick"]
138
      or dev_props["device.nick"]
139
      or dev_props["api.alsa.card.name"]
140
      or dev_props["alsa.card_name"]
141
142
  -- also sanitize nick, replace ':' with ' '
  properties["node.nick"] = nick:gsub("(:)", " ")
143
144
145

  -- ensure the node has a description
  if not properties["node.description"] then
146
147
148
    local desc = nonempty(dev_props["device.description"]) or "unknown"
    local name = nonempty(properties["api.alsa.pcm.name"]) or
                 nonempty(properties["api.alsa.pcm.id"]) or dev
149
150

    if profile_desc then
151
      desc = desc .. " " .. profile_desc
152
    elseif subdev ~= "0" then
153
      desc = desc .. " (" .. name .. " " .. subdev .. ")"
154
    elseif dev ~= "0" then
155
      desc = desc .. " (" .. name .. ")"
156
    end
157
158
159

    -- also sanitize description, replace ':' with ' '
    properties["node.description"] = desc:gsub("(:)", " ")
160
161
  end

162
163
164
165
166
167
168
  -- add api.alsa.card.* properties for rule matching purposes
  for k, v in pairs(dev_props) do
    if k:find("^api%.alsa%.card%..*") then
      properties[k] = v
    end
  end

169
170
  -- apply properties from config.rules
  rulesApplyProperties(properties)
171
172
173
174
175
176
177

  -- create the node
  local node = Node("adapter", properties)
  node:activate(Feature.Proxy.BOUND)
  parent:store_managed_object(id, node)
end

178
179
180
181
182
183
184
185
186
187
188
189
190
function createDevice(parent, id, factory, properties)
  local device = SpaDevice(factory, properties)
  device:connect("create-object", createNode)
  device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
  parent:store_managed_object(id, device)
end

function prepareDevice(parent, id, type, factory, properties)
  -- ensure the device has an appropriate name
  local name = "alsa_card." ..
    (properties["device.name"] or
     properties["device.bus-id"] or
     properties["device.bus-path"] or
191
     tostring(id)):gsub("([^%w_%-%.])", "_")
192
193
194
195
196
197
198
199
200
201

  properties["device.name"] = name

  -- deduplicate devices with the same name
  for counter = 2, 99, 1 do
    if findDuplicate(parent, id, "device.name", properties["device.name"]) then
      properties["device.name"] = name .. "." .. counter
    else
      break
    end
202
203
204
205
206
207
208
209
210
211
212
213
214
215
  end

  -- ensure the device has a description
  if not properties["device.description"] then
    local d = nil
    local f = properties["device.form-factor"]
    local c = properties["device.class"]

    if f == "internal" then
      d = "Built-in Audio"
    elseif c == "modem" then
      d = "Modem"
    end

216
217
218
219
    d = d or properties["device.product.name"]
          or properties["api.alsa.card.name"]
          or properties["alsa.card_name"]
          or "Unknown device"
220
221
222
    properties["device.description"] = d
  end

223
224
225
226
227
  -- ensure the device has a nick
  properties["device.nick"] =
      properties["device.nick"] or
      properties["api.alsa.card.name"]

228
229
230
  -- set the icon name
  if not properties["device.icon-name"] then
    local icon = nil
231
232
233
234
235
236
237
238
239
240
241
242
    local icon_map = {
      -- form factor -> icon
      ["microphone"] = "audio-input-microphone",
      ["webcam"] = "camera-web",
      ["handset"] = "phone",
      ["portable"] = "multimedia-player",
      ["tv"] = "video-display",
      ["headset"] = "audio-headset",
      ["headphone"] = "audio-headphones",
      ["speaker"] = "audio-speakers",
      ["hands-free"] = "audio-handsfree",
    }
243
244
245
246
    local f = properties["device.form-factor"]
    local c = properties["device.class"]
    local b = properties["device.bus"]

247
248
    icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
    properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
249
250
  end

251
252
253
  -- apply properties from config.rules
  rulesApplyProperties(properties)

254
  -- override the device factory to use ACP
255
256
  if properties["api.alsa.use-acp"] then
    Log.info("Enabling the use of ACP on " .. properties["device.name"])
257
258
259
    factory = "api.alsa.acp.device"
  end

260
  -- use device reservation, if available
261
  if rd_plugin and properties["api.alsa.card"] then
262
263
    local rd_name = "Audio" .. properties["api.alsa.card"]
    local rd = rd_plugin:call("create-reservation",
264
265
266
267
        rd_name,
        config.properties["alsa.reserve.application-name"] or "WirePlumber",
        properties["device.name"],
        config.properties["alsa.reserve.priority"] or -20);
268
269
270
271
272
273
274
275
276
277

    properties["api.dbus.ReserveDevice1"] = rd_name

    -- unlike pipewire-media-session, this logic here keeps the device
    -- acquired at all times and destroys it if someone else acquires
    rd:connect("notify::state", function (rd, pspec)
      local state = rd["state"]

      if state == "acquired" then
        -- create the device
278
        createDevice(parent, id, factory, properties)
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303

      elseif state == "available" then
        -- attempt to acquire again
        rd:call("acquire")

      elseif state == "busy" then
        -- destroy the device
        parent:store_managed_object(id, nil)
      end
    end)

    if jack_device then
      rd:connect("notify::owner-name-changed", function (rd, pspec)
        if rd["state"] == "busy" and
           rd["owner-application-name"] == "Jack audio server" then
            -- TODO enable the jack device
        else
            -- TODO disable the jack device
        end
      end)
    end

    rd:call("acquire")
  else
    -- create the device
304
    createDevice(parent, id, factory, properties)
305
  end
306
307
end

308
monitor = SpaDevice("api.alsa.enum.udev", config.properties)
309
310
311
312
313
314
315
316

if monitor then
  monitor:connect("create-object", prepareDevice)
else
  Log.message("PipeWire's SPA ALSA udev plugin(\"api.alsa.enum.udev\") missing or broken. Sound Cards Cannot be enumerated.")
  return false
end

317

318
319
320
321
322
323
-- create the JACK device (for PipeWire to act as client to a JACK server)
if config.properties["alsa.jack-device"] then
  jack_device = Device("spa-device-factory", {
    ["factory.name"] = "api.jack.device",
    ["node.name"] = "JACK-Device",
  })
324
  jack_device:activate(Feature.Proxy.BOUND)
325
end
326

327
328
-- enable device reservation if requested
if config.properties["alsa.reserve"] then
329
  rd_plugin = Plugin.find("reserve-device")
330
331
end

332
333
334
335
-- if the reserve-device plugin is enabled, at the point of script execution
-- it is expected to be connected. if it is not, assume the d-bus connection
-- has failed and continue without it
if rd_plugin and rd_plugin["state"] ~= "connected" then
336
337
  Log.message("reserve-device plugin is not connected to D-Bus, "
              .. "disabling device reservation")
338
339
  rd_plugin = nil
end
340

341
-- destroy device reservations when the corresponding devices are removed
342
343
344
345
346
347
348
349
if rd_plugin then
  monitor:connect("object-removed", function (parent, id)
    local device = parent:get_managed_object(id)
    local rd_name = device.properties["api.dbus.ReserveDevice1"]
    if rd_name then
      rd_plugin:call("destroy-reservation", rd_name)
    end
  end)
350
end
351
352
353

Log.info("Activating ALSA monitor")
monitor:activate(Feature.SpaDevice.ENABLED)