diff --git a/src/config/config.lua b/src/config/config.lua
index 274250f60225c4b0be776b6a30e295de350c6a08..8a306e3464d663e235786c4c1ceb80198be68120 100644
--- a/src/config/config.lua
+++ b/src/config/config.lua
@@ -86,14 +86,6 @@ function enable_audio()
 
   -- Enables device reservation via org.freedesktop.ReserveDevice1 on D-Bus
   load_module("reserve-device")
-
-  -- ALSA device management via udev
-  load_monitor("alsa", {
-    use_acp = true,
-    use_device_reservation = true,
-    enable_midi = true,
-    enable_jack_client = false,
-  })
 end
 
 function enable_endpoints()
diff --git a/src/config/config.lua.d/30-alsa-monitor.lua b/src/config/config.lua.d/30-alsa-monitor.lua
new file mode 100644
index 0000000000000000000000000000000000000000..32521771ed58b3fdb26baf1666df69945e93ca9d
--- /dev/null
+++ b/src/config/config.lua.d/30-alsa-monitor.lua
@@ -0,0 +1,104 @@
+-- ALSA monitor config file --
+
+local properties = {
+  -- Create a JACK device. This is not enabled by default because
+  -- it requires that the PipeWire JACK replacement libraries are
+  -- not used by the session manager, in order to be able to
+  -- connect to the real JACK server.
+  --["alsa.jack-device"] = false,
+
+  -- Reserve devices.
+  --["alsa.reserve"] = true,
+  --["alsa.reserve.priority"] = -20,
+  --["alsa.reserve.application-name"] = "WirePlumber",
+}
+
+local rules = {
+  -- An array of matches/actions to evaluate.
+  {
+    -- Rules for matching a device or node. It is an array of
+    -- properties that all need to match the regexp. If any of the
+    -- matches work, the actions are executed for the object.
+    matches = {
+      {
+        -- This matches all cards.
+        { "device.name", "matches", "alsa_card.*" },
+      },
+    },
+    -- Apply properties on the matched object.
+    apply_properties = {
+      -- Use ALSA-Card-Profile devices. They use UCM or the profile
+      -- configuration to configure the device and mixer settings.
+      ["api.alsa.use-acp"] = true,
+
+      -- Use UCM instead of profile when available. Can be
+      -- disabled to skip trying to use the UCM profile.
+      --["api.alsa.use-ucm"] = true,
+
+      -- Don't use the hardware mixer for volume control. It
+      -- will only use software volume. The mixer is still used
+      -- to mute unused paths based on the selected port.
+      --["api.alsa.soft-mixer"] = false,
+
+      -- Ignore decibel settings of the driver. Can be used to
+      -- work around buggy drivers that report wrong values.
+      --["api.alsa.ignore-dB"] = false,
+
+      -- The profile set to use for the device. Usually this is
+      -- "default.conf" but can be changed with a udev rule or here.
+      --["device.profile-set"] = "profileset-name",
+
+      -- The default active profile. Is by default set to "Off".
+      --["device.profile"] = "default profile name",
+
+      -- Automatically select the best profile. This is the
+      -- highest priority available profile. This is disabled
+      -- here and instead implemented in the session manager
+      -- where it can save and load previous preferences.
+      ["api.acp.auto-profile"] = false,
+
+      -- Automatically switch to the highest priority available port.
+      -- This is disabled here and implemented in the session manager instead.
+      ["api.acp.auto-port"] = false,
+
+      -- Other properties can be set here.
+      --["device.nick"] = "My Device",
+    },
+  },
+  {
+    matches = {
+      {
+        -- Matches all sources.
+        { "node.name", "matches", "alsa_input.*" },
+      },
+      {
+        -- Matches all sinks.
+        { "node.name", "matches", "alsa_output.*" },
+      },
+    },
+    apply_properties = {
+      --["node.nick"]              = "My Node",
+      --["priority.driver"]        = 100,
+      --["priority.session"]       = 100,
+      --["node.pause-on-idle"]     = false,
+      --["resample.quality"]       = 4,
+      --["channelmix.normalize"]   = false,
+      --["channelmix.mix-lfe"]     = false,
+      --["audio.channels"]         = 2,
+      --["audio.format"]           = "S16LE",
+      --["audio.rate"]             = 44100,
+      --["audio.position"]         = "FL,FR",
+      --["api.alsa.period-size"]   = 1024,
+      --["api.alsa.headroom"]      = 0,
+      --["api.alsa.disable-mmap"]  = false,
+      --["api.alsa.disable-batch"] = false,
+    }
+  }
+}
+
+function enable_alsa()
+  load_monitor("alsa", {
+    properties = properties,
+    rules = rules,
+  })
+end
diff --git a/src/config/config.lua.d/90-enable-audio-all.lua b/src/config/config.lua.d/90-enable-audio-all.lua
index 93e75dcf8f8a0949d6a41c4383b229ab3f7c3632..b4dad7a12d27e38a98633281641acc11b737e607 100644
--- a/src/config/config.lua.d/90-enable-audio-all.lua
+++ b/src/config/config.lua.d/90-enable-audio-all.lua
@@ -1,2 +1,3 @@
 enable_audio()
+enable_alsa()
 enable_bluetooth()
diff --git a/src/scripts/monitors/monitor-alsa.lua b/src/scripts/monitors/monitor-alsa.lua
index 4324c3eb2b0038d20de99faac5e457e4bd2d7ce7..9153b6f9a845377c5580feaa84dae0cb3d00cdcf 100644
--- a/src/scripts/monitors/monitor-alsa.lua
+++ b/src/scripts/monitors/monitor-alsa.lua
@@ -6,33 +6,94 @@
 -- SPDX-License-Identifier: MIT
 
 -- Receive script arguments from config.lua
-local Config = ...
-
-if Config.enable_midi then
-  midi_bridge = Node("spa-node-factory", {
-    ["factory.name"] = "api.alsa.seq.bridge",
-    ["node.name"] = "MIDI Bridge"
-  })
+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
 end
 
-if Config.enable_jack_client then
-  jack_device = Device("spa-device-factory", {
-    ["factory.name"] = "api.jack.device"
-  })
+-- 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
 end
 
-if Config.use_device_reservation then
-  rd_plugin = Plugin("reserve-device")
+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
 end
 
 function createNode(parent, id, type, factory, properties)
   local dev_props = parent.properties
-  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"
+
+  -- 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"
   local stream = properties["api.alsa.pcm.stream"] or "unknown"
-  local profile = properties["device.profile.name"] or "unknown"
+  local profile = properties["device.profile.name"]
+                  or (stream .. "." .. dev .. "." .. subdev)
   local profile_desc = properties["device.profile.description"]
 
+  -- 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
+
   -- ensure the node has a media class
   if not properties["media.class"] then
     if stream == "capture" then
@@ -43,14 +104,34 @@ function createNode(parent, id, type, factory, properties)
   end
 
   -- ensure the node has a name
+  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
+
+    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
   properties["node.nick"] = properties["node.nick"]
       or dev_props["device.nick"]
-      or dev_props["api.alsa.card_name"]
+      or dev_props["api.alsa.card.name"]
       or dev_props["alsa.card_name"]
 
-  properties["node.name"] = properties["node.name"]
-      or (dev_props["device.name"] or "unknown") .. "." .. stream .. "." .. dev .. "." .. subdev
-
   -- ensure the node has a description
   if not properties["node.description"] then
     local desc = dev_props["device.description"] or "unknown"
@@ -67,9 +148,8 @@ function createNode(parent, id, type, factory, properties)
     end
   end
 
-  -- set the device id and spa factory name; REQUIRED, do not change
-  properties["device.id"] = parent["bound-id"]
-  properties["factory.name"] = factory
+  -- apply properties from config.rules
+  rulesApplyProperties(properties)
 
   -- create the node
   local node = Node("adapter", properties)
@@ -77,11 +157,30 @@ function createNode(parent, id, type, factory, properties)
   parent:store_managed_object(id, node)
 end
 
-function createDevice(parent, id, type, factory, properties)
-  -- ensure the device has a name
-  if not properties["device.name"] then
-    local s = properties["device.bus-id"] or properties["device.bus-path"] or "unknown"
-    properties["device.name"] = "alsa_card." .. s
+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
+     tostring(id))
+
+  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
   end
 
   -- ensure the device has a description
@@ -96,55 +195,58 @@ function createDevice(parent, id, type, factory, properties)
       d = "Modem"
     end
 
-    d = d or properties["device.product.name"] or "Unknown device"
+    d = d or properties["device.product.name"]
+          or properties["api.alsa.card.name"]
+          or properties["alsa.card_name"]
+          or "Unknown device"
     properties["device.description"] = d
   end
 
+  -- ensure the device has a nick
+  properties["device.nick"] =
+      properties["device.nick"] or
+      properties["api.alsa.card.name"]
+
   -- set the icon name
   if not properties["device.icon-name"] then
     local icon = nil
+    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",
+    }
     local f = properties["device.form-factor"]
     local c = properties["device.class"]
     local b = properties["device.bus"]
 
-    if f == "microphone" then
-      icon = "audio-input-microphone"
-    elseif f == "webcam" then
-      icon = "camera-web"
-    elseif f == "handset" then
-      icon = "phone"
-    elseif f == "portable" then
-      icon = "multimedia-player"
-    elseif f == "tv" then
-      icon = "video-display"
-    elseif f == "headset" then
-      icon = "audio-headset"
-    elseif f == "headphone" then
-      icon = "audio-headphones"
-    elseif f == "speaker" then
-      icon = "audio-speakers"
-    elseif f == "hands-free" then
-      icon = "audio-handsfree"
-    elseif c == "modem" then
-      icon = "modem"
-    end
-
-    icon = icon or "audio-card"
-
-    if b then b = ("-" .. b) else b = "" end
-    properties["device.icon-name"] = icon .. "-analog" .. b
+    icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
+    properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
   end
 
+  -- apply properties from config.rules
+  rulesApplyProperties(properties)
+
   -- override the device factory to use ACP
-  if Config.use_acp then
+  if properties["api.alsa.use-acp"] then
+    Log.info("Enabling the use of ACP on " .. properties["device.name"])
     factory = "api.alsa.acp.device"
   end
 
   -- use device reservation, if available
-  if rd_plugin then
+  if rd_plugin and properties["api.alsa.card"] then
     local rd_name = "Audio" .. properties["api.alsa.card"]
     local rd = rd_plugin:call("create-reservation",
-        rd_name, "WirePlumber", properties["device.name"], -20);
+        rd_name,
+        config.properties["alsa.reserve.application-name"] or "WirePlumber",
+        properties["device.name"],
+        config.properties["alsa.reserve.priority"] or -20);
 
     properties["api.dbus.ReserveDevice1"] = rd_name
 
@@ -155,10 +257,7 @@ function createDevice(parent, id, type, factory, properties)
 
       if state == "acquired" then
         -- create the device
-        local device = SpaDevice(factory, properties)
-        device:connect("create-object", createNode)
-        device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
-        parent:store_managed_object(id, device)
+        createDevice(parent, id, factory, properties)
 
       elseif state == "available" then
         -- attempt to acquire again
@@ -184,38 +283,37 @@ function createDevice(parent, id, type, factory, properties)
     rd:call("acquire")
   else
     -- create the device
-    local device = SpaDevice(factory, properties)
-    device:connect("create-object", createNode)
-    device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
-    parent:store_managed_object(id, device)
+    createDevice(parent, id, factory, properties)
   end
 end
 
-monitor = SpaDevice("api.alsa.enum.udev")
-monitor:connect("create-object", createDevice)
+monitor = SpaDevice("api.alsa.enum.udev", config.properties)
+monitor:connect("create-object", prepareDevice)
 
-function activateMonitor()
-  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)
-  end
+-- 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",
+  })
+end
 
-  Log.info("Activating ALSA monitor")
-  monitor:activate(Feature.SpaDevice.ENABLED)
+-- reservation is only disabled by explicitly setting it to false
+if config.properties["alsa.reserve"] == true or
+   config.properties["alsa.reserve"] == nil then
+  rd_plugin = Plugin("reserve-device")
 end
 
 -- 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
+  Log.message("reserve-device plugin is not connected to D-Bus, "
+              .. "disabling device reservation")
   rd_plugin = nil
 end
 
+-- destroy device reservations when the corresponding devices are removed
 if rd_plugin then
   monitor:connect("object-removed", function (parent, id)
     local device = parent:get_managed_object(id)