diff --git a/modules/meson.build b/modules/meson.build
index 9b7b2d5ba6e6dbd7c185914032689bd922931328..662142eda9bb61190fa4d4f9e68ed9fc717651fa 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -54,7 +54,8 @@ shared_library(
   'wireplumber-module-pw-audio-softdsp-endpoint',
   [
     'module-pw-audio-softdsp-endpoint.c',
-  ],
+    'module-pw-audio-softdsp-endpoint/dsp.c',
+ ],
   c_args : [common_c_args, '-DG_LOG_DOMAIN="m-pw-audio-softdsp-endpoint"'],
   install : true,
   install_dir : wireplumber_module_dir,
diff --git a/modules/module-mixer.c b/modules/module-mixer.c
index b9e995866dcd1898d8a57c545baeab153c940a79..5a5153f2974d3ba420679463deaab711d4782815 100644
--- a/modules/module-mixer.c
+++ b/modules/module-mixer.c
@@ -11,10 +11,10 @@
 G_DECLARE_FINAL_TYPE (WpMixerEndpoint,
     mixer_endpoint, WP, MIXER_ENDPOINT, WpEndpoint)
 
-static const char * streams[] = {
-  "Master"
+enum {
+  PROP_0,
+  PROP_STREAMS,
 };
-#define N_STREAMS (sizeof (streams) / sizeof (const char *))
 
 enum {
   CONTROL_VOLUME = 0,
@@ -28,7 +28,7 @@ enum {
 struct group
 {
   WpMixerEndpoint *mixer;
-  const gchar *name;
+  gchar *name;
   guint32 mixer_stream_id;
 
   GWeakRef backend;
@@ -38,7 +38,8 @@ struct group
 struct _WpMixerEndpoint
 {
   WpEndpoint parent;
-  struct group groups[N_STREAMS];
+  GVariant *streams;
+  GArray *groups;
 };
 
 G_DEFINE_TYPE (WpMixerEndpoint, mixer_endpoint, WP_TYPE_ENDPOINT)
@@ -104,14 +105,15 @@ policy_changed (WpPolicyManager *mgr, WpMixerEndpoint * self)
   int i;
   g_autoptr (WpCore) core = wp_endpoint_get_core (WP_ENDPOINT (self));
 
-  for (i = 0; i < N_STREAMS; i++) {
-    group_find_backend (&self->groups[i], core);
+  for (i = 0; i < self->groups->len; i++) {
+    group_find_backend (&g_array_index (self->groups, struct group, i), core);
   }
 }
 
 static void
 mixer_endpoint_init (WpMixerEndpoint * self)
 {
+  self->groups = g_array_new (FALSE, TRUE, sizeof (struct group));
 }
 
 static void
@@ -120,18 +122,27 @@ mixer_endpoint_constructed (GObject * object)
   WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object);
   g_autoptr (WpCore) core = NULL;
   g_autoptr (WpPolicyManager) policymgr = NULL;
+  struct group empty_group = {0};
   GVariantDict d;
+  GVariantIter iter;
   gint i;
+  gchar *stream;
 
   core = wp_endpoint_get_core (WP_ENDPOINT (self));
   policymgr = wp_policy_manager_get_instance (core);
   g_signal_connect_object (policymgr, "policy-changed",
       (GCallback) policy_changed, self, 0);
 
-  for (i = 0; i < N_STREAMS; i++) {
+  g_variant_iter_init (&iter, self->streams);
+  for (i = 0; g_variant_iter_next (&iter, "s", &stream); i++) {
+    struct group *group;
+
+    g_array_append_val (self->groups, empty_group);
+    group = &g_array_index (self->groups, struct group, i);
+
     g_variant_dict_init (&d, NULL);
     g_variant_dict_insert (&d, "id", "u", i);
-    g_variant_dict_insert (&d, "name", "s", streams[i]);
+    g_variant_dict_insert (&d, "name", "s", stream);
     wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d));
 
     g_variant_dict_init (&d, NULL);
@@ -151,12 +162,12 @@ mixer_endpoint_constructed (GObject * object)
     g_variant_dict_insert (&d, "default-value", "b", FALSE);
     wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
 
-    self->groups[i].mixer = self;
-    self->groups[i].name = streams[i];
-    self->groups[i].mixer_stream_id = i;
-    g_weak_ref_init (&self->groups[i].backend, NULL);
+    group->mixer = self;
+    group->name = stream;
+    group->mixer_stream_id = i;
+    g_weak_ref_init (&group->backend, NULL);
 
-    group_find_backend (&self->groups[i], core);
+    group_find_backend (group, core);
   }
 
   G_OBJECT_CLASS (mixer_endpoint_parent_class)->constructed (object);
@@ -168,16 +179,37 @@ mixer_endpoint_finalize (GObject * object)
   WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object);
   gint i;
 
-  for (i = 0; i < N_STREAMS; i++) {
-    g_autoptr (WpEndpoint) backend = g_weak_ref_get (&self->groups[i].backend);
+  for (i = 0; i < self->groups->len; i++) {
+    struct group *group = &g_array_index (self->groups, struct group, i);
+    g_autoptr (WpEndpoint) backend = g_weak_ref_get (&group->backend);
     if (backend)
-      g_signal_handlers_disconnect_by_data (backend, &self->groups[i]);
-    g_weak_ref_clear (&self->groups[i].backend);
+      g_signal_handlers_disconnect_by_data (backend, group);
+    g_weak_ref_clear (&group->backend);
+    g_free (group->name);
   }
 
+  g_clear_pointer (&self->groups, g_array_unref);
+  g_clear_pointer (&self->streams, g_variant_unref);
+
   G_OBJECT_CLASS (mixer_endpoint_parent_class)->finalize (object);
 }
 
+static void
+mixer_endpoint_set_property (GObject * object, guint property_id,
+    const GValue * value, GParamSpec * pspec)
+{
+  WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object);
+
+  switch (property_id) {
+  case PROP_STREAMS:
+    self->streams = g_value_dup_variant (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
 static GVariant *
 mixer_endpoint_get_control_value (WpEndpoint * ep, guint32 control_id)
 {
@@ -189,12 +221,12 @@ mixer_endpoint_get_control_value (WpEndpoint * ep, guint32 control_id)
   stream_id = control_id / N_CONTROLS;
   control_id = control_id % N_CONTROLS;
 
-  if (stream_id >= N_STREAMS) {
+  if (stream_id >= self->groups->len) {
     g_warning ("Mixer:%p Invalid stream id %u", self, stream_id);
     return NULL;
   }
 
-  group = &self->groups[stream_id];
+  group = &g_array_index (self->groups, struct group, stream_id);
   backend = g_weak_ref_get (&group->backend);
 
   /* if there is no backend, return the default value */
@@ -228,12 +260,12 @@ mixer_endpoint_set_control_value (WpEndpoint * ep, guint32 control_id,
   stream_id = control_id / N_CONTROLS;
   control_id = control_id % N_CONTROLS;
 
-  if (stream_id >= N_STREAMS) {
+  if (stream_id >= self->groups->len) {
     g_warning ("Mixer:%p Invalid stream id %u", self, stream_id);
     return FALSE;
   }
 
-  group = &self->groups[stream_id];
+  group = &g_array_index (self->groups, struct group, stream_id);
   backend = g_weak_ref_get (&group->backend);
 
   if (!backend) {
@@ -251,20 +283,29 @@ mixer_endpoint_class_init (WpMixerEndpointClass * klass)
   GObjectClass *object_class = (GObjectClass *) klass;
   WpEndpointClass *endpoint_class = (WpEndpointClass *) klass;
 
+  object_class->set_property = mixer_endpoint_set_property;
   object_class->constructed = mixer_endpoint_constructed;
   object_class->finalize = mixer_endpoint_finalize;
 
   endpoint_class->get_control_value = mixer_endpoint_get_control_value;
   endpoint_class->set_control_value = mixer_endpoint_set_control_value;
+
+  g_object_class_install_property (object_class, PROP_STREAMS,
+      g_param_spec_variant ("streams", "streams",
+          "The stream names for the streams to create",
+          G_VARIANT_TYPE ("as"), NULL,
+          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 }
 
 static void
-remote_connected (WpRemote *remote, WpRemoteState state, WpCore *core)
+remote_connected (WpRemote *remote, WpRemoteState state, GVariant *streams)
 {
+  g_autoptr (WpCore) core = wp_remote_get_core (remote);
   g_autoptr (WpEndpoint) ep = g_object_new (mixer_endpoint_get_type (),
       "core", core,
       "name", "Mixer",
       "media-class", "Mixer/Audio",
+      "streams", streams,
       NULL);
   wp_endpoint_register (ep);
 }
@@ -273,10 +314,14 @@ void
 wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
 {
   WpRemote *remote;
+  GVariant *streams;
 
   remote = wp_core_get_global (core, WP_GLOBAL_REMOTE_PIPEWIRE);
   g_return_if_fail (remote != NULL);
 
-  g_signal_connect (remote, "state-changed::connected",
-      (GCallback) remote_connected, core);
+  streams = g_variant_lookup_value (args, "streams", G_VARIANT_TYPE ("as"));
+
+  g_signal_connect_data (remote, "state-changed::connected",
+      (GCallback) remote_connected, streams, (GClosureNotify) g_variant_unref,
+      0);
 }
diff --git a/modules/module-pw-alsa-udev.c b/modules/module-pw-alsa-udev.c
index a353cad4636d2ce6d5e971748d93be3a11426647..ab29eaed9dda8105568541841cbc6ae4c6320da2 100644
--- a/modules/module-pw-alsa-udev.c
+++ b/modules/module-pw-alsa-udev.c
@@ -20,6 +20,7 @@ struct impl
   WpModule *module;
   WpRemotePipewire *remote_pipewire;
   GHashTable *registered_endpoints;
+  GVariant *streams;
 };
 
 static void
@@ -73,6 +74,8 @@ on_node_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p,
       "media-class", g_variant_new_string (media_class));
   g_variant_builder_add (&b, "{sv}",
       "global-id", g_variant_new_uint32 (id));
+  g_variant_builder_add (&b, "{sv}",
+      "streams", impl->streams);
   endpoint_props = g_variant_builder_end (&b);
 
   /* Create the endpoint async */
@@ -110,6 +113,8 @@ module_destroy (gpointer data)
   g_hash_table_unref(impl->registered_endpoints);
   impl->registered_endpoints = NULL;
 
+  g_clear_pointer (&impl->streams, g_variant_unref);
+
   /* Clean up */
   g_slice_free (struct impl, impl);
 }
@@ -134,6 +139,8 @@ wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
   impl->remote_pipewire = rp;
   impl->registered_endpoints = g_hash_table_new_full (g_direct_hash,
       g_direct_equal, NULL, (GDestroyNotify)g_object_unref);
+  impl->streams = g_variant_lookup_value (args, "streams",
+      G_VARIANT_TYPE ("as"));
 
   /* Set destroy callback for impl */
   wp_module_set_destroy_callback (module, module_destroy, impl);
diff --git a/modules/module-pw-audio-softdsp-endpoint.c b/modules/module-pw-audio-softdsp-endpoint.c
index 49a5c6c0d066cbacb927262103136c133926d8f8..496e130029673dcdf7c291393cc65e4098e57d6e 100644
--- a/modules/module-pw-audio-softdsp-endpoint.c
+++ b/modules/module-pw-audio-softdsp-endpoint.c
@@ -19,15 +19,22 @@
 #include <spa/pod/builder.h>
 #include <spa/param/props.h>
 
+#include "module-pw-audio-softdsp-endpoint/dsp.h"
+
 #define MIN_QUANTUM_SIZE  64
 #define MAX_QUANTUM_SIZE  1024
+#define CONTROL_SELECTED 0
 
 struct _WpPwAudioSoftdspEndpoint
 {
   WpEndpoint parent;
 
-  /* The global-id this endpoint refers to */
+  /* Properties */
   guint global_id;
+  GVariant *streams;
+
+  guint stream_count;
+  gboolean selected;
 
   /* The task to signal the endpoint is initialized */
   GTask *init_task;
@@ -35,40 +42,22 @@ struct _WpPwAudioSoftdspEndpoint
   /* The remote pipewire */
   WpRemotePipewire *remote_pipewire;
 
-  /* Handler */
-  gulong proxy_dsp_done_handler_id;
-
-  /* temporary method to select which endpoint
-   * is going to be the default input/output */
-  gboolean selected;
-
   /* Direction */
   enum pw_direction direction;
 
   /* Proxies */
   WpProxyNode *proxy_node;
   WpProxyPort *proxy_port;
-  WpProxyNode *proxy_dsp;
-  GPtrArray *proxies_dsp_port;
-
-  /* Volume */
-  gfloat master_volume;
-  gboolean master_mute;
 
-  /* DSP */
-  struct spa_hook dsp_listener;
-  struct pw_proxy *link_proxy;
+  /* Audio Dsp */
+  WpPwAudioDsp *converter;
+  GPtrArray *dsps;
 };
 
 enum {
   PROP_0,
   PROP_GLOBAL_ID,
-};
-
-enum {
-  CONTROL_VOLUME = 0,
-  CONTROL_MUTE,
-  CONTROL_SELECTED,
+  PROP_STREAMS,
 };
 
 static GAsyncInitableIface *wp_endpoint_parent_interface = NULL;
@@ -82,174 +71,27 @@ G_DEFINE_TYPE_WITH_CODE (WpPwAudioSoftdspEndpoint, endpoint, WP_TYPE_ENDPOINT,
     G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
                            wp_endpoint_async_initable_init))
 
-static void
-proxies_dsp_port_foreach_func(gpointer data, gpointer user_data)
-{
-  GVariantBuilder *b = user_data;
-  g_variant_builder_add (b, "t", data);
-}
-
 static gboolean
 endpoint_prepare_link (WpEndpoint * ep, guint32 stream_id,
     WpEndpointLink * link, GVariant ** properties, GError ** error)
 {
   WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
-  const struct pw_node_info *dsp_info = NULL;
-  GVariantBuilder b, *b_ports;
-  GVariant *v_ports;
-
-  /* Get the dsp info */
-  dsp_info = wp_proxy_node_get_info(self->proxy_dsp);
-  g_return_val_if_fail (dsp_info, FALSE);
-
-  /* Create a variant array with all the ports */
-  b_ports = g_variant_builder_new (G_VARIANT_TYPE ("at"));
-  g_ptr_array_foreach(self->proxies_dsp_port, proxies_dsp_port_foreach_func,
-      b_ports);
-  v_ports = g_variant_builder_end (b_ports);
-
-  /* Set the properties */
-  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
-  g_variant_builder_add (&b, "{sv}", "node-id",
-      g_variant_new_uint32 (dsp_info->id));
-  g_variant_builder_add (&b, "{sv}", "ports", v_ports);
-  *properties = g_variant_builder_end (&b);
-
-  return TRUE;
-}
+  WpPwAudioDsp *stream = NULL;
 
-static void
-on_dsp_running(WpPwAudioSoftdspEndpoint *self)
-{
-  struct pw_properties *props;
-  const struct pw_node_info *node_info = NULL;
-  const struct pw_node_info *dsp_info = NULL;
-
-  /* Return if the node has already been linked */
-  g_return_if_fail (!self->link_proxy);
-
-  /* Get the node info */
-  node_info = wp_proxy_node_get_info(self->proxy_node);
-  g_return_if_fail (node_info);
-
-  /* Get the dsp info */
-  dsp_info = wp_proxy_node_get_info(self->proxy_dsp);
-  g_return_if_fail (dsp_info);
-
-  /* Create new properties */
-  props = pw_properties_new(NULL, NULL);
-
-  /* Set the new properties */
-  pw_properties_set(props, PW_LINK_PROP_PASSIVE, "true");
-  if (self->direction == PW_DIRECTION_OUTPUT) {
-    pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", dsp_info->id);
-    pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1);
-    pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", node_info->id);
-    pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1);
-  } else {
-    pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", node_info->id);
-    pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1);
-    pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", dsp_info->id);
-    pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1);
-  }
-
-  g_debug ("%p linking DSP to node", self);
-
-  /* Create the link */
-  self->link_proxy = wp_remote_pipewire_create_object(self->remote_pipewire,
-      "link-factory", PW_TYPE_INTERFACE_Link, &props->dict);
-
-  /* Clean up */
-  pw_properties_free(props);
-}
-
-static void
-on_dsp_idle (WpPwAudioSoftdspEndpoint *self)
-{
-  if (self->link_proxy != NULL) {
-    g_debug ("%p unlinking DSP from node", self);
-    pw_proxy_destroy (self->link_proxy);
-    self->link_proxy = NULL;
-  }
-}
-
-static void
-dsp_node_event_info (void *data, const struct pw_node_info *info)
-{
-  WpPwAudioSoftdspEndpoint *self = data;
-
-  /* Handle the different states */
-  switch (info->state) {
-  case PW_NODE_STATE_IDLE:
-    on_dsp_idle (self);
-    break;
-  case PW_NODE_STATE_RUNNING:
-    on_dsp_running (self);
-    break;
-  case PW_NODE_STATE_SUSPENDED:
-    break;
-  default:
-    break;
-  }
-}
+  /* Make sure the stream Id is valid */
+  g_return_val_if_fail(stream_id < self->dsps->len, FALSE);
 
-static void
-dsp_node_event_param (void *object, int seq, uint32_t id,
-    uint32_t index, uint32_t next, const struct spa_pod *param)
-{
-  WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object);
+  /* Make sure the stream is valid */
+  stream = g_ptr_array_index (self->dsps, stream_id);
+  g_return_val_if_fail(stream, FALSE);
 
-  switch (id) {
-    case SPA_PARAM_Props:
-    {
-      struct spa_pod_prop *prop;
-      struct spa_pod_object *obj = (struct spa_pod_object *) param;
-      float volume = self->master_volume;
-      bool mute = self->master_mute;
-
-      SPA_POD_OBJECT_FOREACH(obj, prop) {
-        switch (prop->key) {
-        case SPA_PROP_volume:
-          spa_pod_get_float(&prop->value, &volume);
-          break;
-        case SPA_PROP_mute:
-          spa_pod_get_bool(&prop->value, &mute);
-          break;
-        default:
-          break;
-        }
-      }
-
-      g_debug ("WpEndpoint:%p param event, vol:(%lf -> %f) mute:(%d -> %d)",
-          self, self->master_volume, volume, self->master_mute, mute);
-
-      if (self->master_volume != volume) {
-        self->master_volume = volume;
-        wp_endpoint_notify_control_value (WP_ENDPOINT (self), CONTROL_VOLUME);
-      }
-      if (self->master_mute != mute) {
-        self->master_mute = mute;
-        wp_endpoint_notify_control_value (WP_ENDPOINT (self), CONTROL_MUTE);
-      }
-
-      break;
-    }
-    default:
-      break;
-  }
+  /* Prepare the link */
+  return wp_pw_audio_dsp_prepare_link (stream, properties, error);
 }
 
-static const struct pw_node_proxy_events dsp_node_events = {
-  PW_VERSION_NODE_PROXY_EVENTS,
-  .info = dsp_node_event_info,
-  .param = dsp_node_event_param,
-};
-
 static void
-on_proxy_dsp_done(WpProxy *proxy, gpointer data)
+finish_endpoint_creation(WpPwAudioSoftdspEndpoint *self)
 {
-  WpPwAudioSoftdspEndpoint *self = data;
-
   /* Don't do anything if the endpoint has already been initialized */
   if (!self->init_task)
     return;
@@ -260,90 +102,79 @@ on_proxy_dsp_done(WpProxy *proxy, gpointer data)
 }
 
 static void
-on_proxy_dsp_created(GObject *initable, GAsyncResult *res, gpointer data)
+on_audio_dsp_stream_created(GObject *initable, GAsyncResult *res, gpointer data)
 {
   WpPwAudioSoftdspEndpoint *self = data;
-  struct pw_node_proxy *dsp_proxy = NULL;
-  const struct spa_audio_info_raw *port_format;
-  struct spa_audio_info_raw format;
-  uint8_t buf[1024];
-  struct spa_pod_builder pod_builder = { 0, };
-  struct spa_pod *param;
-
-  /* Get the proxy dsp */
-  self->proxy_dsp = wp_proxy_node_new_finish(initable, res, NULL);
-  g_return_if_fail (self->proxy_dsp);
-
-  /* Add a custom dsp listener */
-  dsp_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy_dsp));
-  g_return_if_fail (dsp_proxy);
-  pw_node_proxy_add_listener(dsp_proxy, &self->dsp_listener,
-      &dsp_node_events, self);
-
-  /* Emit the props param */
-  pw_node_proxy_enum_params (dsp_proxy, 0, SPA_PARAM_Props, 0, -1, NULL);
-
-  /* Get the port format */
-  port_format = wp_proxy_port_get_format(self->proxy_port);
-  g_return_if_fail (port_format);
-  format = *port_format;
-
-  /* Build the param profile */
-  spa_pod_builder_init(&pod_builder, buf, sizeof(buf));
-  param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &format);
-  param = spa_pod_builder_add_object(&pod_builder,
-      SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
-      SPA_PARAM_PROFILE_direction,  SPA_POD_Id(pw_direction_reverse(self->direction)),
-      SPA_PARAM_PROFILE_format,     SPA_POD_Pod(param));
-
-  /* Set the param profile to emit the dsp ports */
-  pw_node_proxy_set_param(dsp_proxy, SPA_PARAM_Profile, 0, param);
+  WpPwAudioDsp *dsp = NULL;
+  guint stream_id = 0;
+  g_autofree gchar *name = NULL;
+
+  /* Get the stream */
+  dsp = wp_pw_audio_dsp_new_finish(initable, res, NULL);
+  g_return_if_fail (dsp);
+
+  /* Get the stream id */
+  g_object_get (dsp, "id", &stream_id, "name", &name, NULL);
+  g_return_if_fail (stream_id >= 0);
+
+  /* Set the streams */
+  g_ptr_array_insert (self->dsps, stream_id, dsp);
+
+  g_debug ("%s:%p Created stream %u %s", G_OBJECT_TYPE_NAME (self), self,
+      stream_id, name);
+
+  /* Finish the endpoint creation when all the streams are created */
+  if (--self->stream_count == 0)
+    finish_endpoint_creation(self);
 }
 
 static void
-emit_audio_dsp_node (WpPwAudioSoftdspEndpoint *self)
+on_audio_dsp_converter_created(GObject *initable, GAsyncResult *res,
+    gpointer data)
 {
-  struct pw_properties *props;
-  const char *dsp_name = NULL;
-  struct pw_node_proxy *dsp_proxy = NULL;
-  const struct pw_node_info *node_info;
-
-  /* Get the node info */
-  node_info = wp_proxy_node_get_info(self->proxy_node);
-  g_return_if_fail (node_info);
-
-  /* Get the properties */
-  props = pw_properties_new_dict(node_info->props);
-  g_return_if_fail (props);
-
-  /* Get the DSP name */
-  dsp_name = pw_properties_get(props, "device.nick");
-  if (!dsp_name)
-    dsp_name = node_info->name;
-
-  /* Set the properties */
-  pw_properties_set(props, "audio-dsp.name", dsp_name);
-  pw_properties_setf(props, "audio-dsp.direction", "%d", self->direction);
-  pw_properties_setf(props, "audio-dsp.maxbuffer", "%ld",
-      MAX_QUANTUM_SIZE * sizeof(float));
-
-  /* Create the proxy dsp async */
-  dsp_proxy = wp_remote_pipewire_create_object(self->remote_pipewire,
-      "audio-dsp", PW_TYPE_INTERFACE_Node, &props->dict);
-  wp_proxy_node_new(pw_proxy_get_id((struct pw_proxy *)dsp_proxy), dsp_proxy,
-      on_proxy_dsp_created, self);
-
-  /* Clean up */
-  pw_properties_free(props);
+  WpPwAudioSoftdspEndpoint *self = data;
+  g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self));
+  const struct pw_node_info *target = NULL;
+  const struct spa_audio_info_raw *format = NULL;
+  GVariantDict d;
+  GVariantIter iter;
+  const gchar *stream;
+  int i;
+
+  /* Get the proxy dsp converter */
+  self->converter = wp_pw_audio_dsp_new_finish(initable, res, NULL);
+  g_return_if_fail (self->converter);
+
+  /* Get the target and format */
+  target = wp_pw_audio_dsp_get_info (self->converter);
+  g_return_if_fail (target);
+  g_object_get (self->converter, "format", &format, NULL);
+  g_return_if_fail (format);
+
+  /* Create the audio dsp streams */
+  g_variant_iter_init (&iter, self->streams);
+  for (i = 0; g_variant_iter_next (&iter, "&s", &stream); i++) {
+    wp_pw_audio_dsp_new (WP_ENDPOINT(self), i, stream, self->direction,
+        FALSE, target, format, on_audio_dsp_stream_created, self);
+
+    /* Register the stream */
+    g_variant_dict_init (&d, NULL);
+    g_variant_dict_insert (&d, "id", "u", i);
+    g_variant_dict_insert (&d, "name", "s", stream);
+    wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d));
+  }
+  self->stream_count = i;
 }
 
 static void
 on_proxy_node_created(GObject *initable, GAsyncResult *res, gpointer data)
 {
   WpPwAudioSoftdspEndpoint *self = data;
-  GVariantDict d;
+  g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self));
   g_autofree gchar *name = NULL;
   const struct spa_dict *props;
+  const struct pw_node_info *target = NULL;
+  const struct spa_audio_info_raw *format = NULL;
 
   /* Get the proxy node */
   self->proxy_node = wp_proxy_node_new_finish(initable, res, NULL);
@@ -358,38 +189,16 @@ on_proxy_node_created(GObject *initable, GAsyncResult *res, gpointer data)
       wp_proxy_node_get_info (self->proxy_node)->id);
   g_object_set (self, "name", name, NULL);
 
-  /* Emit the audio DSP node */
-  emit_audio_dsp_node(self);
-
-  g_variant_dict_init (&d, NULL);
-  g_variant_dict_insert (&d, "id", "u", 0);
-  g_variant_dict_insert (&d, "name", "s", "default");
-  wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d));
-
-  self->master_volume = 1.0;
-  g_variant_dict_init (&d, NULL);
-  g_variant_dict_insert (&d, "id", "u", CONTROL_VOLUME);
-  g_variant_dict_insert (&d, "name", "s", "volume");
-  g_variant_dict_insert (&d, "type", "s", "d");
-  g_variant_dict_insert (&d, "range", "(dd)", 0.0, 1.0);
-  g_variant_dict_insert (&d, "default-value", "d", self->master_volume);
-  wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
-
-  self->master_mute = FALSE;
-  g_variant_dict_init (&d, NULL);
-  g_variant_dict_insert (&d, "id", "u", CONTROL_MUTE);
-  g_variant_dict_insert (&d, "name", "s", "mute");
-  g_variant_dict_insert (&d, "type", "s", "b");
-  g_variant_dict_insert (&d, "default-value", "b", self->master_mute);
-  wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
-
-  self->selected = FALSE;
-  g_variant_dict_init (&d, NULL);
-  g_variant_dict_insert (&d, "id", "u", CONTROL_SELECTED);
-  g_variant_dict_insert (&d, "name", "s", "selected");
-  g_variant_dict_insert (&d, "type", "s", "b");
-  g_variant_dict_insert (&d, "default-value", "b", self->selected);
-  wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
+  /* Create the converter proxy */
+  target = wp_proxy_node_get_info (self->proxy_node);
+  g_return_if_fail (target);
+  format = wp_proxy_port_get_format (self->proxy_port);
+  g_return_if_fail (format);
+  /* TODO: For now we create convert as a stream because convert mode does not
+   * generate any ports, not sure why */
+  wp_pw_audio_dsp_new (WP_ENDPOINT(self), WP_STREAM_ID_NONE, "master",
+      self->direction, TRUE, target, format, on_audio_dsp_converter_created,
+      self);
 }
 
 static void
@@ -410,11 +219,16 @@ on_proxy_port_created(GObject *initable, GAsyncResult *res, gpointer data)
 }
 
 static void
-handle_node_port(WpPwAudioSoftdspEndpoint *self, guint id, guint parent_id,
-  const struct spa_dict *props)
+on_port_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p,
+    gpointer d)
 {
+  WpPwAudioSoftdspEndpoint *self = d;
   struct pw_port_proxy *port_proxy = NULL;
 
+  /* Check if it is a node port and handle it */
+  if (self->global_id != parent_id)
+    return;
+
   /* Alsa nodes should have 1 port only, so make sure proxy_port is not set */
   if (self->proxy_port != 0)
     return;
@@ -426,74 +240,12 @@ handle_node_port(WpPwAudioSoftdspEndpoint *self, guint id, guint parent_id,
   wp_proxy_port_new(id, port_proxy, on_proxy_port_created, self);
 }
 
-static void
-on_proxy_dsp_port_created(GObject *initable, GAsyncResult *res, gpointer data)
-{
-  WpPwAudioSoftdspEndpoint *self = data;
-  WpProxyPort *proxy_dsp_port = NULL;
-
-  /* Get the proxy dsp port */
-  proxy_dsp_port = wp_proxy_port_new_finish(initable, res, NULL);
-  g_return_if_fail (proxy_dsp_port);
-
-  /* Add the proxy dsp port to the array */
-  g_return_if_fail (self->proxies_dsp_port);
-  g_ptr_array_add(self->proxies_dsp_port, proxy_dsp_port);
-
-  /* Register a callback to know when all the dsp ports have been emitted */
-  if (!self->proxy_dsp_done_handler_id) {
-    self->proxy_dsp_done_handler_id = g_signal_connect_object(self->proxy_dsp,
-        "done", (GCallback)on_proxy_dsp_done, self, 0);
-    wp_proxy_sync (WP_PROXY(self->proxy_dsp));
-  }
-}
-
-static void
-handle_dsp_port(WpPwAudioSoftdspEndpoint *self, guint id, guint parent_id,
-  const struct spa_dict *props)
-{
-  struct pw_port_proxy *port_proxy = NULL;
-
-  /* Create the proxy dsp port async */
-  port_proxy = wp_remote_pipewire_proxy_bind (self->remote_pipewire, id,
-      PW_TYPE_INTERFACE_Port);
-  g_return_if_fail(port_proxy);
-  wp_proxy_port_new(id, port_proxy, on_proxy_dsp_port_created, self);
-}
-
-static void
-on_port_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p,
-    gpointer d)
-{
-  WpPwAudioSoftdspEndpoint *self = d;
-  const struct spa_dict *props = p;
-  const struct pw_node_info *dsp_info = NULL;
-
-  /* Check if it is a node port and handle it */
-  if (self->global_id == parent_id) {
-    handle_node_port(self, id, parent_id, props);
-    return;
-  }
-
-  /* Otherwise, check if it is a dsp port and handle it */
-  if (!self->proxy_dsp)
-    return;
-  dsp_info = wp_proxy_node_get_info (self->proxy_dsp);
-  if (!dsp_info || dsp_info->id != parent_id)
-    return;
-  handle_dsp_port(self, id, parent_id, props);
-}
-
 static void
 endpoint_finalize (GObject * object)
 {
   WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object);
 
-  /* Destroy the proxies port */
-  if (self->proxies_dsp_port) {
-    g_ptr_array_free(self->proxies_dsp_port, TRUE);
-    self->proxies_dsp_port = NULL;
-  }
+  g_clear_pointer(&self->streams, g_variant_unref);
 
   /* Destroy the proxy node */
   g_clear_object(&self->proxy_node);
@@ -501,8 +253,11 @@ endpoint_finalize (GObject * object)
   /* Destroy the proxy port */
   g_clear_object(&self->proxy_port);
 
-  /* Destroy the proxy dsp */
-  g_clear_object(&self->proxy_dsp);
+  /* Destroy the proxy dsp converter */
+  g_clear_object(&self->converter);
+
+  /* Destroy all the proxy dsp streams */
+  g_clear_pointer (&self->dsps, g_ptr_array_unref);
 
   /* Destroy the done task */
   g_clear_object(&self->init_task);
@@ -520,6 +275,9 @@ endpoint_set_property (GObject * object, guint property_id,
   case PROP_GLOBAL_ID:
     self->global_id = g_value_get_uint(value);
     break;
+  case PROP_STREAMS:
+    self->streams = g_value_dup_variant(value);
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     break;
@@ -536,6 +294,9 @@ endpoint_get_property (GObject * object, guint property_id,
   case PROP_GLOBAL_ID:
     g_value_set_uint (value, self->global_id);
     break;
+  case PROP_STREAMS:
+    g_value_set_variant (value, self->streams);
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     break;
@@ -543,83 +304,52 @@ endpoint_get_property (GObject * object, guint property_id,
 }
 
 static GVariant *
-endpoint_get_control_value (WpEndpoint * ep, guint32 control_id)
+endpoint_get_control_value (WpEndpoint * ep, guint32 id)
 {
   WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
+  guint stream_id, control_id;
+  WpPwAudioDsp *stream = NULL;
 
-  switch (control_id) {
-    case CONTROL_VOLUME:
-      return g_variant_new_double (self->master_volume);
-    case CONTROL_MUTE:
-      return g_variant_new_boolean (self->master_mute);
-    case CONTROL_SELECTED:
-      return g_variant_new_boolean (self->selected);
-    default:
-      g_warning ("Unknown control id %u", control_id);
-      return NULL;
-  }
+  if (id == CONTROL_SELECTED)
+    return g_variant_new_boolean (self->selected);
+
+  wp_pw_audio_dsp_id_decode (id, &stream_id, &control_id);
+
+  /* Check if it is the master stream */
+  if (stream_id == WP_STREAM_ID_NONE)
+    return wp_pw_audio_dsp_get_control_value (self->converter, control_id);
+
+  /* Otherwise get the stream_id and control_id */
+  g_return_val_if_fail (stream_id < self->dsps->len, NULL);
+  stream = g_ptr_array_index (self->dsps, stream_id);
+  g_return_val_if_fail (stream, NULL);
+  return wp_pw_audio_dsp_get_control_value (stream, control_id);
 }
 
 static gboolean
-endpoint_set_control_value (WpEndpoint * ep, guint32 control_id,
-    GVariant * value)
+endpoint_set_control_value (WpEndpoint * ep, guint32 id, GVariant * value)
 {
   WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
-  struct pw_node_proxy *dsp_proxy = NULL;
-  float volume;
-  bool mute;
-
-  /* Get the pipewire dsp proxy */
-  g_return_val_if_fail (self->proxy_dsp, FALSE);
-  dsp_proxy = wp_proxy_get_pw_proxy (WP_PROXY(self->proxy_dsp));
-  g_return_val_if_fail (dsp_proxy, FALSE);
-
-  switch (control_id) {
-    case CONTROL_VOLUME:
-      volume = g_variant_get_double (value);
-
-      g_debug("WpEndpoint:%p set volume control (%u) value, vol:%f", self,
-          control_id, volume);
-
-      pw_node_proxy_set_param (dsp_proxy,
-          SPA_PARAM_Props, 0,
-          spa_pod_builder_add_object (&b,
-              SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
-              SPA_PROP_volume, SPA_POD_Float(volume),
-              NULL));
-      pw_node_proxy_enum_params (dsp_proxy, 0, SPA_PARAM_Props, 0, -1,
-          NULL);
-      break;
-
-    case CONTROL_MUTE:
-      mute = g_variant_get_boolean (value);
-
-      g_debug("WpEndpoint:%p set mute control (%u) value, mute:%d", self,
-          control_id, mute);
-
-      pw_node_proxy_set_param (dsp_proxy,
-          SPA_PARAM_Props, 0,
-          spa_pod_builder_add_object (&b,
-              SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
-              SPA_PROP_mute, SPA_POD_Bool(mute),
-              NULL));
-      pw_node_proxy_enum_params (dsp_proxy, 0, SPA_PARAM_Props, 0, -1,
-          NULL);
-      break;
-
-    case CONTROL_SELECTED:
-      self->selected = g_variant_get_boolean (value);
-      wp_endpoint_notify_control_value (ep, CONTROL_SELECTED);
-      break;
-
-    default:
-      g_warning ("Unknown control id %u", control_id);
-      return FALSE;
+  guint stream_id, control_id;
+  WpPwAudioDsp *stream = NULL;
+
+  if (id == CONTROL_SELECTED) {
+    self->selected = g_variant_get_boolean (value);
+    wp_endpoint_notify_control_value (ep, CONTROL_SELECTED);
+    return TRUE;
   }
 
-  return TRUE;
+  wp_pw_audio_dsp_id_decode (id, &stream_id, &control_id);
+
+  /* Check if it is the master stream */
+  if (stream_id == WP_STREAM_ID_NONE)
+    return wp_pw_audio_dsp_set_control_value (self->converter, control_id, value);
+
+  /* Otherwise get the stream_id and control_id */
+  g_return_val_if_fail (stream_id < self->dsps->len, FALSE);
+  stream = g_ptr_array_index (self->dsps, stream_id);
+  g_return_val_if_fail (stream, FALSE);
+  return wp_pw_audio_dsp_set_control_value (stream, control_id, value);
 }
 
 static void
@@ -629,13 +359,11 @@ wp_endpoint_init_async (GAsyncInitable *initable, int io_priority,
   WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (initable);
   g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self));
   const gchar *media_class = wp_endpoint_get_media_class (WP_ENDPOINT (self));
+  GVariantDict d;
 
   /* Create the async task */
   self->init_task = g_task_new (initable, cancellable, callback, data);
 
-  /* Init the proxies_dsp_port array */
-  self->proxies_dsp_port = g_ptr_array_new_full(4, (GDestroyNotify)g_object_unref);
-
   /* Set the direction */
   if (g_str_has_suffix (media_class, "Source"))
     self->direction = PW_DIRECTION_INPUT;
@@ -650,6 +378,15 @@ wp_endpoint_init_async (GAsyncInitable *initable, int io_priority,
   g_signal_connect_object(self->remote_pipewire, "global-added::port",
       (GCallback)on_port_added, self, 0);
 
+  /* Register the selected control */
+  self->selected = FALSE;
+  g_variant_dict_init (&d, NULL);
+  g_variant_dict_insert (&d, "id", "u", CONTROL_SELECTED);
+  g_variant_dict_insert (&d, "name", "s", "selected");
+  g_variant_dict_insert (&d, "type", "s", "b");
+  g_variant_dict_insert (&d, "default-value", "b", self->selected);
+  wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
+
   /* Call the parent interface */
   wp_endpoint_parent_interface->init_async (initable, io_priority, cancellable,
       callback, data);
@@ -670,6 +407,7 @@ wp_endpoint_async_initable_init (gpointer iface, gpointer iface_data)
 static void
 endpoint_init (WpPwAudioSoftdspEndpoint * self)
 {
+  self->dsps = g_ptr_array_new_with_free_func (g_object_unref);
 }
 
 static void
@@ -691,6 +429,12 @@ endpoint_class_init (WpPwAudioSoftdspEndpointClass * klass)
       g_param_spec_uint ("global-id", "global-id",
           "The global Id this endpoint refers to", 0, G_MAXUINT, 0,
           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (object_class, PROP_STREAMS,
+      g_param_spec_variant ("streams", "streams",
+          "The stream names for the streams to create",
+          G_VARIANT_TYPE ("as"), NULL,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 }
 
 void
@@ -700,6 +444,7 @@ endpoint_factory (WpFactory * factory, GType type, GVariant * properties,
   g_autoptr (WpCore) core = NULL;
   const gchar *media_class;
   guint global_id;
+  g_autoptr (GVariant) streams = NULL;
 
   /* Make sure the type is correct */
   g_return_if_fail(type == WP_TYPE_ENDPOINT);
@@ -713,6 +458,9 @@ endpoint_factory (WpFactory * factory, GType type, GVariant * properties,
       return;
   if (!g_variant_lookup (properties, "global-id", "u", &global_id))
       return;
+  if (!(streams = g_variant_lookup_value (properties, "streams",
+          G_VARIANT_TYPE ("as"))))
+      return;
 
   /* Create and return the softdsp endpoint object */
   g_async_initable_new_async (
@@ -720,6 +468,7 @@ endpoint_factory (WpFactory * factory, GType type, GVariant * properties,
       "core", core,
       "media-class", media_class,
       "global-id", global_id,
+      "streams", streams,
       NULL);
 }
 
diff --git a/modules/module-pw-audio-softdsp-endpoint/dsp.c b/modules/module-pw-audio-softdsp-endpoint/dsp.c
new file mode 100644
index 0000000000000000000000000000000000000000..0c0f6b18915a270e699d29f3ba9027f1b9dda812
--- /dev/null
+++ b/modules/module-pw-audio-softdsp-endpoint/dsp.c
@@ -0,0 +1,740 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/builder.h>
+#include <spa/param/props.h>
+
+#include "dsp.h"
+
+#define MIN_QUANTUM_SIZE  64
+#define MAX_QUANTUM_SIZE  1024
+
+enum {
+  PROP_0,
+  PROP_ENDPOINT,
+  PROP_ID,
+  PROP_NAME,
+  PROP_DIRECTION,
+  PROP_CONVERT,
+  PROP_TARGET,
+  PROP_FORMAT,
+};
+
+enum {
+  CONTROL_VOLUME = 0,
+  CONTROL_MUTE,
+  N_CONTROLS,
+};
+
+struct _WpPwAudioDsp
+{
+  GObject parent;
+
+  /* The task to signal the audio dsp is initialized */
+  GTask *init_task;
+
+  /* The remote pipewire */
+  WpRemotePipewire *remote_pipewire;
+
+  /* Props */
+  GWeakRef endpoint;
+  guint id;
+  gchar *name;
+  enum pw_direction direction;
+  gboolean convert;
+  const struct pw_node_info *target;
+  const struct spa_audio_info_raw *format;
+
+  /* All ports handled by the port added callback */
+  GHashTable *handled_ports;
+
+  /* Proxies */
+  WpProxyNode *proxy;
+  GPtrArray *port_proxies;
+  struct pw_proxy *link_proxy;
+
+  /* Listener */
+  struct spa_hook listener;
+
+  /* Volume */
+  gfloat volume;
+  gboolean mute;
+};
+
+static void wp_pw_audio_dsp_async_initable_init (gpointer iface,
+    gpointer iface_data);
+
+G_DEFINE_TYPE_WITH_CODE (WpPwAudioDsp, wp_pw_audio_dsp, G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
+                           wp_pw_audio_dsp_async_initable_init))
+
+guint
+wp_pw_audio_dsp_id_encode (guint stream_id, guint control_id)
+{
+  g_return_val_if_fail (control_id < N_CONTROLS, 0);
+
+  /* encode NONE as 0 and everything else with +1 */
+  /* NONE is MAX_UINT, so +1 will do the trick */
+  stream_id += 1;
+
+  /* Encode the stream and control Ids. The first ID is reserved
+   * for the "selected" control, registered in the endpoint */
+  return 1 + (stream_id * N_CONTROLS) + control_id;
+}
+
+void
+wp_pw_audio_dsp_id_decode (guint id, guint *stream_id, guint *control_id)
+{
+  guint s_id, c_id;
+
+  g_return_if_fail (id >= 1);
+  id -= 1;
+
+  /* Decode the stream and control Ids */
+  s_id = (id / N_CONTROLS) - 1;
+  c_id = id % N_CONTROLS;
+
+  /* Set the output params */
+  if (stream_id)
+    *stream_id = s_id;
+  if (control_id)
+    *control_id = c_id;
+}
+
+static void
+register_controls (WpPwAudioDsp * self)
+{
+  GVariantDict d;
+  g_autoptr (WpEndpoint) ep = g_weak_ref_get (&self->endpoint);
+  g_return_if_fail (ep);
+
+  /* Register the volume control */
+  g_variant_dict_init (&d, NULL);
+  g_variant_dict_insert (&d, "id", "u",
+      wp_pw_audio_dsp_id_encode (self->id, CONTROL_VOLUME));
+  if (self->id != WP_STREAM_ID_NONE)
+    g_variant_dict_insert (&d, "stream-id", "u", self->id);
+  g_variant_dict_insert (&d, "name", "s", "volume");
+  g_variant_dict_insert (&d, "type", "s", "d");
+  g_variant_dict_insert (&d, "range", "(dd)", 0.0, 1.0);
+  g_variant_dict_insert (&d, "default-value", "d", self->volume);
+  wp_endpoint_register_control (ep, g_variant_dict_end (&d));
+
+  /* Register the mute control */
+  g_variant_dict_init (&d, NULL);
+  g_variant_dict_insert (&d, "id", "u",
+      wp_pw_audio_dsp_id_encode (self->id, CONTROL_MUTE));
+  if (self->id != WP_STREAM_ID_NONE)
+    g_variant_dict_insert (&d, "stream-id", "u", self->id);
+  g_variant_dict_insert (&d, "name", "s", "mute");
+  g_variant_dict_insert (&d, "type", "s", "b");
+  g_variant_dict_insert (&d, "default-value", "b", self->mute);
+  wp_endpoint_register_control (ep, g_variant_dict_end (&d));
+}
+
+static void
+on_audio_dsp_port_created(GObject *initable, GAsyncResult *res,
+    gpointer data)
+{
+  WpPwAudioDsp *self = data;
+  WpProxyPort *port_proxy = NULL;
+
+  /* Get the proxy port */
+  port_proxy = wp_proxy_port_new_finish(initable, res, NULL);
+  g_return_if_fail (port_proxy);
+
+  /* Add the proxy port to the array */
+  g_return_if_fail (self->port_proxies);
+  g_ptr_array_add(self->port_proxies, port_proxy);
+}
+
+static void
+handled_ports_foreach_func (gpointer key, gpointer value, gpointer data)
+{
+  WpPwAudioDsp *self = data;
+  const struct pw_node_info *dsp_info = NULL;
+  struct pw_port_proxy *port_proxy = NULL;
+  const guint id = GPOINTER_TO_INT (key);
+  const guint parent_id = GPOINTER_TO_INT (value);
+
+  /* Get the dsp info */
+  g_return_if_fail (self->proxy);
+  dsp_info = wp_proxy_node_get_info(self->proxy);
+  g_return_if_fail (dsp_info);
+
+  /* Skip ports that are not owned by this DSP */
+  if (dsp_info->id != parent_id)
+    return;
+
+  /* Create the audio dsp port async */
+  port_proxy = wp_remote_pipewire_proxy_bind (self->remote_pipewire, id,
+      PW_TYPE_INTERFACE_Port);
+  g_return_if_fail(port_proxy);
+  wp_proxy_port_new(id, port_proxy, on_audio_dsp_port_created, self);
+}
+
+static void
+on_audio_dsp_done(WpProxy *proxy, gpointer data)
+{
+  WpPwAudioDsp *self = data;
+
+  g_return_if_fail (self->proxy);
+
+  /* Don't do anything if the endpoint has already been initialized */
+  if (!self->init_task)
+      return;
+
+  /* Create the proxis and sync to trigger this function again */
+  if (self->port_proxies->len == 0) {
+    g_hash_table_foreach (self->handled_ports, handled_ports_foreach_func, self);
+    wp_proxy_sync (WP_PROXY(self->proxy));
+    return;
+  }
+
+  /* Register the controls */
+  register_controls (self);
+
+  /* Finish the creation of the audio dsp */
+  g_task_return_boolean (self->init_task, TRUE);
+  g_clear_object(&self->init_task);
+}
+
+static void
+on_audio_dsp_port_added(WpRemotePipewire *rp, guint id, guint parent_id,
+    gconstpointer p, gpointer d)
+{
+  WpPwAudioDsp *self = d;
+
+  /* Add the port to the map if it is not already there */
+  if (!g_hash_table_contains (self->handled_ports, GUINT_TO_POINTER (id)))
+    g_hash_table_insert (self->handled_ports, GUINT_TO_POINTER(id),
+      GUINT_TO_POINTER(parent_id));
+}
+
+static void
+on_audio_dsp_running(WpPwAudioDsp *self)
+{
+  struct pw_properties *props;
+  const struct pw_node_info *dsp_info = NULL;
+
+  /* Return if the node has already been linked */
+  if (self->link_proxy)
+    return;
+
+  /* Get the dsp info */
+  dsp_info = wp_proxy_node_get_info(self->proxy);
+  g_return_if_fail (dsp_info);
+
+  /* Create new properties */
+  props = pw_properties_new(NULL, NULL);
+
+  /* Set the new properties */
+  pw_properties_set(props, PW_LINK_PROP_PASSIVE, "true");
+  if (self->direction == PW_DIRECTION_OUTPUT) {
+    pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", dsp_info->id);
+    pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1);
+    pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", self->target->id);
+    pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1);
+  } else {
+    pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", self->target->id);
+    pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1);
+    pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", dsp_info->id);
+    pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1);
+  }
+
+  g_debug ("%p linking DSP to node", self);
+
+  /* Create the link */
+  self->link_proxy = wp_remote_pipewire_create_object(self->remote_pipewire,
+      "link-factory", PW_TYPE_INTERFACE_Link, &props->dict);
+
+  /* Clean up */
+  pw_properties_free(props);
+}
+
+static void
+on_audio_dsp_idle (WpPwAudioDsp *self)
+{
+  if (self->link_proxy != NULL) {
+    pw_proxy_destroy (self->link_proxy);
+    self->link_proxy = NULL;
+  }
+}
+
+static void
+audio_dsp_event_info (void *data, const struct pw_node_info *info)
+{
+  WpPwAudioDsp *self = data;
+
+  /* Handle the different states */
+  switch (info->state) {
+  case PW_NODE_STATE_IDLE:
+    on_audio_dsp_idle (self);
+    break;
+  case PW_NODE_STATE_RUNNING:
+    on_audio_dsp_running (self);
+    break;
+  case PW_NODE_STATE_SUSPENDED:
+    break;
+  default:
+    break;
+  }
+}
+
+static void
+audio_dsp_event_param (void *object, int seq, uint32_t id,
+    uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+  WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object);
+  g_autoptr (WpEndpoint) ep = g_weak_ref_get (&self->endpoint);
+
+  switch (id) {
+    case SPA_PARAM_Props:
+    {
+      struct spa_pod_prop *prop;
+      struct spa_pod_object *obj = (struct spa_pod_object *) param;
+      float volume = self->volume;
+      bool mute = self->mute;
+
+      SPA_POD_OBJECT_FOREACH(obj, prop) {
+        switch (prop->key) {
+        case SPA_PROP_volume:
+          spa_pod_get_float(&prop->value, &volume);
+          break;
+        case SPA_PROP_mute:
+          spa_pod_get_bool(&prop->value, &mute);
+          break;
+        default:
+          break;
+        }
+      }
+
+      g_debug ("WpPwAudioDsp:%p param event, vol:(%lf -> %f) mute:(%d -> %d)",
+          self, self->volume, volume, self->mute, mute);
+
+      if (self->volume != volume) {
+        self->volume = volume;
+        wp_endpoint_notify_control_value (ep,
+            wp_pw_audio_dsp_id_encode (self->id, CONTROL_VOLUME));
+      }
+      if (self->mute != mute) {
+        self->mute = mute;
+        wp_endpoint_notify_control_value (ep,
+            wp_pw_audio_dsp_id_encode (self->id, CONTROL_MUTE));
+      }
+
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+static const struct pw_node_proxy_events audio_dsp_proxy_events = {
+  PW_VERSION_NODE_PROXY_EVENTS,
+  .info = audio_dsp_event_info,
+  .param = audio_dsp_event_param,
+};
+
+static void
+on_audio_dsp_proxy_created(GObject *initable, GAsyncResult *res,
+    gpointer data)
+{
+  WpPwAudioDsp *self = data;
+  struct pw_node_proxy *pw_proxy = NULL;
+  struct spa_audio_info_raw format;
+  uint8_t buf[1024];
+  struct spa_pod_builder pod_builder = { 0, };
+  struct spa_pod *param;
+
+  /* Get the audio dsp proxy */
+  self->proxy = wp_proxy_node_new_finish(initable, res, NULL);
+  g_return_if_fail (self->proxy);
+
+  /* Add a custom dsp listener */
+  pw_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy));
+  g_return_if_fail (pw_proxy);
+  pw_node_proxy_add_listener(pw_proxy, &self->listener,
+      &audio_dsp_proxy_events, self);
+
+  /* Emit the props param */
+  pw_node_proxy_enum_params (pw_proxy, 0, SPA_PARAM_Props, 0, -1, NULL);
+
+  if (!self->convert) {
+    /* Get the port format */
+    g_return_if_fail (self->format);
+    format = *self->format;
+
+    /* Emit the ports */
+    spa_pod_builder_init(&pod_builder, buf, sizeof(buf));
+    param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &format);
+    param = spa_pod_builder_add_object(&pod_builder,
+        SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
+        SPA_PARAM_PROFILE_direction,  SPA_POD_Id(pw_direction_reverse(self->direction)),
+        SPA_PARAM_PROFILE_format,     SPA_POD_Pod(param));
+    pw_node_proxy_set_param(pw_proxy, SPA_PARAM_Profile, 0, param);
+  }
+
+  /* Register a callback to know when all the dsp ports have been emitted */
+  g_signal_connect_object(self->proxy, "done", (GCallback)on_audio_dsp_done,
+      self, 0);
+  wp_proxy_sync (WP_PROXY(self->proxy));
+}
+
+static void
+wp_pw_audio_dsp_finalize (GObject * object)
+{
+  WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object);
+
+  /* Props */
+  g_weak_ref_clear (&self->endpoint);
+  g_free (self->name);
+
+  /* Destroy the init task */
+  g_clear_object(&self->init_task);
+
+  /* Destroy the handled ports map */
+  g_hash_table_unref(self->handled_ports);
+  self->handled_ports = NULL;
+
+  /* Destroy the proxy dsp */
+  g_clear_object(&self->proxy);
+
+  /* Destroy the proxies port */
+  if (self->port_proxies) {
+    g_ptr_array_free(self->port_proxies, TRUE);
+    self->port_proxies = NULL;
+  }
+
+  G_OBJECT_CLASS (wp_pw_audio_dsp_parent_class)->finalize (object);
+}
+
+static void
+wp_pw_audio_dsp_set_property (GObject * object, guint property_id,
+    const GValue * value, GParamSpec * pspec)
+{
+  WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object);
+
+  switch (property_id) {
+  case PROP_ENDPOINT:
+    g_weak_ref_set (&self->endpoint, g_value_get_object (value));
+    break;
+  case PROP_ID:
+    self->id = g_value_get_uint(value);
+    break;
+  case PROP_NAME:
+    self->name = g_value_dup_string (value);
+    break;
+  case PROP_DIRECTION:
+    self->direction = g_value_get_uint(value);
+    break;
+  case PROP_CONVERT:
+    self->convert = g_value_get_boolean(value);
+    break;
+  case PROP_TARGET:
+    self->target = g_value_get_pointer(value);
+    break;
+  case PROP_FORMAT:
+    self->format = g_value_get_pointer(value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+wp_pw_audio_dsp_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object);
+
+  switch (property_id) {
+    case PROP_ENDPOINT:
+    g_value_take_object (value, g_weak_ref_get (&self->endpoint));
+    break;
+  case PROP_ID:
+    g_value_set_uint (value, self->id);
+    break;
+  case PROP_NAME:
+    g_value_set_string (value, self->name);
+    break;
+  case PROP_DIRECTION:
+    g_value_set_uint (value, self->direction);
+    break;
+  case PROP_CONVERT:
+    g_value_set_boolean (value, self->convert);
+    break;
+  case PROP_TARGET:
+    g_value_set_pointer (value, (gpointer)self->target);
+    break;
+  case PROP_FORMAT:
+    g_value_set_pointer (value, (gpointer)self->format);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+wp_pw_audio_dsp_init_async (GAsyncInitable *initable, int io_priority,
+    GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data)
+{
+  WpPwAudioDsp *self = WP_PW_AUDIO_DSP (initable);
+  struct pw_properties *props;
+  struct pw_node_proxy *proxy;
+
+  /* Set the remote pipewire */
+  g_autoptr (WpEndpoint) ep = g_weak_ref_get (&self->endpoint);
+  g_return_if_fail(ep);
+  g_autoptr (WpCore) wp_core = wp_endpoint_get_core(ep);
+  g_return_if_fail(wp_core);
+  self->remote_pipewire =
+      wp_core_get_global (wp_core, WP_GLOBAL_REMOTE_PIPEWIRE);
+  g_return_if_fail(self->remote_pipewire);
+
+  /* Create the async task */
+  self->init_task = g_task_new (initable, cancellable, callback, data);
+
+  /* Init the handled ports map */
+  self->handled_ports = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+  /* Init the list of port proxies */
+  self->port_proxies = g_ptr_array_new_full(4, (GDestroyNotify)g_object_unref);
+
+  /* Set the default volume */
+  self->volume = 1.0;
+  self->mute = FALSE;
+
+  /* Create the properties */
+  props = pw_properties_new_dict(self->target->props);
+  g_return_if_fail (props);
+
+  /* Set the properties */
+  pw_properties_set(props, "audio-dsp.name",
+      self->name ? self->name : "Audio-DSP");
+  pw_properties_set(props, "audio-dsp.mode", self->convert ? "convert" : NULL);
+  pw_properties_setf(props, "audio-dsp.direction", "%d", self->direction);
+  pw_properties_setf(props, "audio-dsp.maxbuffer", "%ld",
+      MAX_QUANTUM_SIZE * sizeof(float));
+
+  /* Register a port_added callback */
+  g_signal_connect_object(self->remote_pipewire, "global-added::port",
+      (GCallback)on_audio_dsp_port_added, self, 0);
+
+  /* Create the proxy async */
+  proxy = wp_remote_pipewire_create_object(self->remote_pipewire,
+      "audio-dsp", PW_TYPE_INTERFACE_Node, &props->dict);
+  wp_proxy_node_new(pw_proxy_get_id((struct pw_proxy *)proxy), proxy,
+      on_audio_dsp_proxy_created, self);
+
+  /* Clean up */
+  pw_properties_free(props);
+}
+
+static gboolean
+wp_pw_audio_dsp_init_finish (GAsyncInitable *initable, GAsyncResult *result,
+    GError **error)
+{
+  g_return_val_if_fail (g_task_is_valid (result, initable), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+wp_pw_audio_dsp_async_initable_init (gpointer iface, gpointer iface_data)
+{
+  GAsyncInitableIface *ai_iface = iface;
+
+  ai_iface->init_async = wp_pw_audio_dsp_init_async;
+  ai_iface->init_finish = wp_pw_audio_dsp_init_finish;
+}
+
+static void
+wp_pw_audio_dsp_init (WpPwAudioDsp * self)
+{
+}
+
+static void
+wp_pw_audio_dsp_class_init (WpPwAudioDspClass * klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  object_class->finalize = wp_pw_audio_dsp_finalize;
+  object_class->set_property = wp_pw_audio_dsp_set_property;
+  object_class->get_property = wp_pw_audio_dsp_get_property;
+
+  /* Install the properties */
+  g_object_class_install_property (object_class, PROP_ENDPOINT,
+      g_param_spec_object ("endpoint", "endpoint",
+          "The endpoint this audio DSP belongs to", WP_TYPE_ENDPOINT,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_ID,
+      g_param_spec_uint ("id", "id", "The Id of the audio DSP", 0, G_MAXUINT, 0,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_NAME,
+      g_param_spec_string ("name", "name", "The name of the audio DSP", NULL,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_DIRECTION,
+      g_param_spec_uint ("direction", "direction",
+          "The direction of the audio DSP", 0, 1, 0,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_CONVERT,
+      g_param_spec_boolean ("convert", "convert",
+          "Whether the DSP is only in convert mode or not", FALSE,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_TARGET,
+      g_param_spec_pointer ("target", "target",
+          "The target node info of the audio DSP",
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_FORMAT,
+      g_param_spec_pointer ("format", "format",
+          "The format of the audio DSP ports",
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+}
+
+void
+wp_pw_audio_dsp_new (WpEndpoint *endpoint, guint id, const char *name,
+    enum pw_direction direction, gboolean convert,
+    const struct pw_node_info *target, const struct spa_audio_info_raw *format,
+    GAsyncReadyCallback callback,
+    gpointer user_data)
+{
+  g_async_initable_new_async (
+      wp_pw_audio_dsp_get_type (), G_PRIORITY_DEFAULT, NULL,
+      callback, user_data,
+      "endpoint", endpoint,
+      "id", id,
+      "name", name,
+      "direction", direction,
+      "convert", convert,
+      "target", target,
+      "format", format,
+      NULL);
+}
+
+WpPwAudioDsp *
+wp_pw_audio_dsp_new_finish (GObject *initable, GAsyncResult *res,
+    GError **error)
+{
+  GAsyncInitable *ai = G_ASYNC_INITABLE(initable);
+  return WP_PW_AUDIO_DSP(g_async_initable_new_finish(ai, res, error));
+}
+
+const struct pw_node_info *
+wp_pw_audio_dsp_get_info (WpPwAudioDsp * self)
+{
+  return wp_proxy_node_get_info(self->proxy);
+}
+
+static void
+port_proxies_foreach_func(gpointer data, gpointer user_data)
+{
+  GVariantBuilder *b = user_data;
+  g_variant_builder_add (b, "t", data);
+}
+
+gboolean
+wp_pw_audio_dsp_prepare_link (WpPwAudioDsp * self, GVariant ** properties,
+    GError ** error) {
+  const struct pw_node_info *info = NULL;
+  GVariantBuilder b, *b_ports;
+  GVariant *v_ports;
+
+  /* Get the proxy node info */
+  info = wp_proxy_node_get_info(self->proxy);
+  g_return_val_if_fail (info, FALSE);
+
+  /* Create a variant array with all the ports */
+  b_ports = g_variant_builder_new (G_VARIANT_TYPE ("at"));
+  g_ptr_array_foreach(self->port_proxies, port_proxies_foreach_func,
+      b_ports);
+  v_ports = g_variant_builder_end (b_ports);
+
+  /* Set the properties */
+  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&b, "{sv}", "node-id",
+      g_variant_new_uint32 (info->id));
+  g_variant_builder_add (&b, "{sv}", "ports", v_ports);
+  *properties = g_variant_builder_end (&b);
+
+  return TRUE;
+}
+
+GVariant *
+wp_pw_audio_dsp_get_control_value (WpPwAudioDsp * self, guint32 control_id)
+{
+  switch (control_id) {
+    case CONTROL_VOLUME:
+      return g_variant_new_double (self->volume);
+    case CONTROL_MUTE:
+      return g_variant_new_boolean (self->mute);
+    default:
+      g_warning ("Unknown control id %u", control_id);
+      return NULL;
+  }
+}
+
+gboolean
+wp_pw_audio_dsp_set_control_value (WpPwAudioDsp * self, guint32 control_id,
+    GVariant * value)
+{
+  char buf[1024];
+  struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
+  struct pw_node_proxy *pw_proxy = NULL;
+  float volume;
+  bool mute;
+
+  /* Get the pipewire dsp proxy */
+  g_return_val_if_fail (self->proxy, FALSE);
+  pw_proxy = wp_proxy_get_pw_proxy (WP_PROXY(self->proxy));
+  g_return_val_if_fail (pw_proxy, FALSE);
+
+  switch (control_id) {
+    case CONTROL_VOLUME:
+      volume = g_variant_get_double (value);
+
+      g_debug("WpPwAudioDsp:%p set volume control (%u) value, vol:%f", self,
+          control_id, volume);
+
+      pw_node_proxy_set_param (pw_proxy,
+          SPA_PARAM_Props, 0,
+          spa_pod_builder_add_object (&b,
+              SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+              SPA_PROP_volume, SPA_POD_Float(volume),
+              NULL));
+      pw_node_proxy_enum_params (pw_proxy, 0, SPA_PARAM_Props, 0, -1,
+          NULL);
+      break;
+
+    case CONTROL_MUTE:
+      mute = g_variant_get_boolean (value);
+
+      g_debug("WpPwAudioDsp:%p set mute control (%u) value, mute:%d", self,
+          control_id, mute);
+
+      pw_node_proxy_set_param (pw_proxy,
+          SPA_PARAM_Props, 0,
+          spa_pod_builder_add_object (&b,
+              SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
+              SPA_PROP_mute, SPA_POD_Bool(mute),
+              NULL));
+      pw_node_proxy_enum_params (pw_proxy, 0, SPA_PARAM_Props, 0, -1,
+          NULL);
+      break;
+
+    default:
+      g_warning ("Unknown control id %u", control_id);
+      return FALSE;
+  }
+
+  return TRUE;
+}
diff --git a/modules/module-pw-audio-softdsp-endpoint/dsp.h b/modules/module-pw-audio-softdsp-endpoint/dsp.h
new file mode 100644
index 0000000000000000000000000000000000000000..5d7c6f44cebfb5ee318058983a24f5d0b452f12d
--- /dev/null
+++ b/modules/module-pw-audio-softdsp-endpoint/dsp.h
@@ -0,0 +1,36 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <gio/gio.h>
+#include <wp/wp.h>
+
+#ifndef __WP_PW_AUDIO_DSP_H__
+#define __WP_PW_AUDIO_DSP_H__
+
+G_DECLARE_FINAL_TYPE (WpPwAudioDsp, wp_pw_audio_dsp,
+    WP_PW, AUDIO_DSP, GObject)
+
+guint wp_pw_audio_dsp_id_encode (guint stream_id, guint control_id);
+void wp_pw_audio_dsp_id_decode (guint id, guint *stream_id, guint *control_id);
+
+void wp_pw_audio_dsp_new (WpEndpoint *endpoint, guint id, const char *name,
+    enum pw_direction direction, gboolean convert,
+    const struct pw_node_info *target, const struct spa_audio_info_raw *format,
+    GAsyncReadyCallback callback, gpointer user_data);
+WpPwAudioDsp * wp_pw_audio_dsp_new_finish (GObject *initable, GAsyncResult *res,
+    GError **error);
+
+const struct pw_node_info *wp_pw_audio_dsp_get_info (WpPwAudioDsp * self);
+gboolean wp_pw_audio_dsp_prepare_link (WpPwAudioDsp * self,
+    GVariant ** properties, GError ** error);
+GVariant * wp_pw_audio_dsp_get_control_value (WpPwAudioDsp * self,
+    guint32 control_id);
+gboolean wp_pw_audio_dsp_set_control_value (WpPwAudioDsp * self,
+    guint32 control_id, GVariant * value);
+
+#endif
diff --git a/modules/module-simple-policy.c b/modules/module-simple-policy.c
index 312f604fd7df852c6c6c0be00283b3b7473cd31d..433819a5ff0f56755c5d14d9d7e66d178db84b2a 100644
--- a/modules/module-simple-policy.c
+++ b/modules/module-simple-policy.c
@@ -316,44 +316,45 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props,
   if (!ptr_array)
     return NULL;
 
-  /* TODO: for now we statically return the first stream
-   * we should be looking into the media.role eventually */
-  g_variant_lookup (props, "media.role", "&s", &role);
-  if (!g_strcmp0 (action, "mixer") && !g_strcmp0 (role, "Master"))
-    *stream_id = WP_STREAM_ID_NONE;
-  else
-    *stream_id = 0;
-
   /* Find and return the "selected" endpoint */
-  /* FIXME: fix the endpoint API, this is terrible */
   for (i = 0; i < ptr_array->len; i++) {
     ep = g_ptr_array_index (ptr_array, i);
-    GVariantIter iter;
-    g_autoptr (GVariant) controls = NULL;
     g_autoptr (GVariant) value = NULL;
-    const gchar *name;
     guint id;
 
-    controls = wp_endpoint_list_controls (ep);
-    g_variant_iter_init (&iter, controls);
-    while ((value = g_variant_iter_next_value (&iter))) {
-      if (!g_variant_lookup (value, "name", "&s", &name)
-          || !g_str_equal (name, "selected")) {
-        g_variant_unref (value);
-        continue;
-      }
-      g_variant_lookup (value, "id", "u", &id);
-      g_variant_unref (value);
-    }
+    id = wp_endpoint_find_control (ep, WP_STREAM_ID_NONE, "selected");
+    if (id == WP_CONTROL_ID_NONE)
+      continue;
 
     value = wp_endpoint_get_control_value (ep, id);
-    if (value && g_variant_get_boolean (value))
-      return g_object_ref (ep);
+    if (value && g_variant_get_boolean (value)) {
+      g_object_ref (ep);
+      goto select_stream;
+    }
   }
 
   /* If not found, return the first endpoint */
-  return (ptr_array->len > 1) ?
+  ep = (ptr_array->len > 1) ?
     g_object_ref (g_ptr_array_index (ptr_array, 0)) : NULL;
+
+select_stream:
+  g_variant_lookup (props, "media.role", "&s", &role);
+  if (!g_strcmp0 (action, "mixer") && !g_strcmp0 (role, "Master"))
+    *stream_id = WP_STREAM_ID_NONE;
+  else if (ep) {
+    /* the default role is "Multimedia" */
+    if (!role)
+      role = "Multimedia";
+    *stream_id = wp_endpoint_find_stream (ep, role);
+
+    /* role not found, try the first stream */
+    if (*stream_id == WP_STREAM_ID_NONE) {
+      g_warning ("role '%s' not found in endpoint", role);
+      *stream_id = 0;
+    }
+  }
+
+  return ep;
 }
 
 static void
diff --git a/src/wireplumber.conf b/src/wireplumber.conf
index 2fef0590d067855a946bcf5af96c502e6746e8c6..999e0e064a259035f0c556ca13de5d9fcdddd2a1 100644
--- a/src/wireplumber.conf
+++ b/src/wireplumber.conf
@@ -9,11 +9,20 @@ load-module C libwireplumber-module-client-permissions
 load-module C libwireplumber-module-pw-audio-softdsp-endpoint
 
 # Endpoint that provides high-level volume controls for the AGL mixer
-load-module C libwireplumber-module-mixer
+# The streams specified here are the ones that will appear in the mixer.
+# They must match the stream names in the alsa-udev module,
+# except for "Master", which is treated specially.
+load-module C libwireplumber-module-mixer {
+  "streams": <["Master", "Multimedia", "Navigation", "Communication", "Emergency"]>
+}
 
 # Monitors the ALSA devices that are discovered via udev
 # and creates softdsp-endopints for each one of them
-load-module C libwireplumber-module-pw-alsa-udev
+# The streams specified here are the ones that will be available for linking
+# clients. Currently, they are matched against the client's role string.
+load-module C libwireplumber-module-pw-alsa-udev {
+  "streams": <["Multimedia", "Navigation", "Communication", "Emergency"]>
+}
 
 # Implements linking clients to devices and maintains
 # information about the devices to be used.