From cfbcb5eca3c0ceb41956e7a1262b1c6588075748 Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Thu, 16 Apr 2020 13:29:48 -0400
Subject: [PATCH] proxy: use WpSpaPod instead of struct spa_pod in public API

---
 lib/wp/core.c                                 |   1 +
 lib/wp/device.c                               |  17 +-
 lib/wp/endpoint-link.c                        |  10 +-
 lib/wp/endpoint-stream.c                      | 194 +++--------
 lib/wp/endpoint-stream.h                      |  38 +--
 lib/wp/endpoint.c                             | 223 ++++---------
 lib/wp/endpoint.h                             |  50 +--
 lib/wp/node.c                                 |  10 +-
 lib/wp/port.c                                 |   5 +-
 lib/wp/private.h                              |  60 +---
 lib/wp/proxy.c                                |  23 +-
 lib/wp/proxy.h                                |  16 +-
 lib/wp/session.c                              | 135 ++++----
 lib/wp/session.h                              |  27 +-
 lib/wp/spa-props.c                            | 311 +++++++-----------
 modules/module-config-policy/config-policy.c  |   8 +-
 modules/module-monitor.c                      |  13 +-
 modules/module-monitor/reservation-data.c     |  24 +-
 modules/module-pipewire/algorithms.c          | 273 ++++++++-------
 .../module-pipewire/audio-softdsp-endpoint.c  |  60 ++--
 .../audio-softdsp-endpoint/adapter.c          |  64 +++-
 .../audio-softdsp-endpoint/convert.c          |  65 ++--
 .../audio-softdsp-endpoint/stream.c           |  31 +-
 .../audio-softdsp-endpoint/stream.h           |   2 +-
 modules/module-si-adapter.c                   |  58 +++-
 tests/modules/algorithms.c                    | 102 +++---
 tests/wp/endpoint.c                           |  64 ++--
 tests/wp/proxy.c                              |   7 +-
 tests/wp/session.c                            |  39 ++-
 tests/wp/spa-props.c                          | 289 ++++++++--------
 tools/wireplumber-cli.c                       |  41 ++-
 31 files changed, 1005 insertions(+), 1255 deletions(-)

diff --git a/lib/wp/core.c b/lib/wp/core.c
index d3299dc5..4520a0cd 100644
--- a/lib/wp/core.c
+++ b/lib/wp/core.c
@@ -274,6 +274,7 @@ wp_core_class_init (WpCoreClass * klass)
   GObjectClass *object_class = (GObjectClass *) klass;
 
   pw_init (NULL, NULL);
+  wp_spa_type_init (TRUE);
 
   object_class->constructed = wp_core_constructed;
   object_class->dispose = wp_core_dispose;
diff --git a/lib/wp/device.c b/lib/wp/device.c
index 481eb78c..d38d9f33 100644
--- a/lib/wp/device.c
+++ b/lib/wp/device.c
@@ -83,14 +83,14 @@ wp_device_get_properties (WpProxy * self)
 
 static gint
 wp_device_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   struct pw_device *pwp;
   int device_enum_params_result;
 
   pwp = (struct pw_device *) wp_proxy_get_pw_proxy (self);
   device_enum_params_result = pw_device_enum_params (pwp, 0, id, start, num,
-      filter);
+      wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (device_enum_params_result >= 0);
 
   return device_enum_params_result;
@@ -98,13 +98,14 @@ wp_device_enum_params (WpProxy * self, guint32 id, guint32 start,
 
 static gint
 wp_device_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   struct pw_device *pwp;
   int device_set_param_result;
 
   pwp = (struct pw_device *) wp_proxy_get_pw_proxy (self);
-  device_set_param_result = pw_device_set_param (pwp, id, flags, param);
+  device_set_param_result = pw_device_set_param (pwp, id, flags,
+      wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (device_set_param_result >= 0);
 
   return device_set_param_result;
@@ -350,13 +351,13 @@ wp_spa_device_get_properties (WpProxy * proxy)
 
 static gint
 wp_spa_device_enum_params (WpProxy * proxy, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   WpSpaDevice *self = WP_SPA_DEVICE (proxy);
   int device_enum_params_result;
 
   device_enum_params_result = spa_device_enum_params (self->interface,
-      0, id, start, num, filter);
+      0, id, start, num, wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (device_enum_params_result >= 0);
 
   return device_enum_params_result;
@@ -364,13 +365,13 @@ wp_spa_device_enum_params (WpProxy * proxy, guint32 id, guint32 start,
 
 static gint
 wp_spa_device_set_param (WpProxy * proxy, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   WpSpaDevice *self = WP_SPA_DEVICE (proxy);
   int device_set_param_result;
 
   device_set_param_result = spa_device_set_param (self->interface,
-      id, flags, param);
+      id, flags, wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (device_set_param_result >= 0);
 
   return device_set_param_result;
diff --git a/lib/wp/endpoint-link.c b/lib/wp/endpoint-link.c
index 24046eca..8e191f71 100644
--- a/lib/wp/endpoint-link.c
+++ b/lib/wp/endpoint-link.c
@@ -87,14 +87,15 @@ wp_endpoint_link_get_properties (WpProxy * proxy)
 
 static gint
 wp_endpoint_link_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   WpEndpointLinkPrivate *priv =
       wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
   int endpoint_link_enum_params_result;
 
   endpoint_link_enum_params_result =
-      pw_endpoint_link_enum_params (priv->iface, 0, id, start, num, filter);
+      pw_endpoint_link_enum_params (priv->iface, 0, id, start, num,
+      wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (endpoint_link_enum_params_result >= 0);
 
   return endpoint_link_enum_params_result;
@@ -116,14 +117,15 @@ wp_endpoint_link_subscribe_params (WpProxy * self, guint32 n_ids, guint32 *ids)
 
 static gint
 wp_endpoint_link_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   WpEndpointLinkPrivate *priv =
       wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
   int endpoint_link_set_param_result;
 
   endpoint_link_set_param_result =
-      pw_endpoint_link_set_param (priv->iface, id, flags, param);
+      pw_endpoint_link_set_param (priv->iface, id, flags,
+          wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (endpoint_link_set_param_result >= 0);
 
   return endpoint_link_set_param_result;
diff --git a/lib/wp/endpoint-stream.c b/lib/wp/endpoint-stream.c
index 88dd12af..72c0abea 100644
--- a/lib/wp/endpoint-stream.c
+++ b/lib/wp/endpoint-stream.c
@@ -110,14 +110,15 @@ wp_endpoint_stream_get_properties (WpProxy * proxy)
 
 static gint
 wp_endpoint_stream_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   WpEndpointStreamPrivate *priv =
       wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
   int endpoint_stream_enum_params_result;
 
   endpoint_stream_enum_params_result =
-      pw_endpoint_stream_enum_params (priv->iface, 0, id, start, num, filter);
+      pw_endpoint_stream_enum_params (priv->iface, 0, id, start, num,
+          wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (endpoint_stream_enum_params_result >= 0);
 
   return endpoint_stream_enum_params_result;
@@ -139,14 +140,15 @@ wp_endpoint_stream_subscribe_params (WpProxy * self, guint32 n_ids, guint32 *ids
 
 static gint
 wp_endpoint_stream_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   WpEndpointStreamPrivate *priv =
       wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
   int endpoint_stream_set_param_result;
 
   endpoint_stream_set_param_result =
-      pw_endpoint_stream_set_param (priv->iface, id, flags, param);
+      pw_endpoint_stream_set_param (priv->iface, id, flags,
+          wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (endpoint_stream_set_param_result >= 0);
 
   return endpoint_stream_set_param_result;
@@ -190,30 +192,29 @@ wp_endpoint_stream_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy
 }
 
 static void
-wp_endpoint_stream_param (WpProxy * proxy, gint seq, guint32 id, guint32 index,
-    guint32 next, const struct spa_pod *param)
+wp_endpoint_stream_param (WpProxy * proxy, gint seq, const gchar * id_name,
+    guint32 index, guint32 next, const WpSpaPod *param)
 {
   WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
   WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-  g_autoptr (GArray) changed_ids = NULL;
-  guint32 prop_id;
+  g_autoptr (GPtrArray) changed_ids = NULL;
+  const gchar * prop_id;
 
-  switch (id) {
-  case SPA_PARAM_PropInfo:
+  if (g_strcmp0 ("PropInfo", id_name) == 0) {
     wp_spa_props_register_from_prop_info (&priv->spa_props, param);
-    break;
-  case SPA_PARAM_Props:
-    changed_ids = g_array_new (FALSE, FALSE, sizeof (uint32_t));
+  }
+
+  else if (g_strcmp0 ("Props", id_name) == 0) {
+    changed_ids = g_ptr_array_new_with_free_func (g_free);
     wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
 
     for (guint i = 0; i < changed_ids->len; i++) {
-      prop_id = g_array_index (changed_ids, uint32_t, i);
+      prop_id = g_ptr_array_index (changed_ids, i);
       g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, prop_id);
     }
 
     wp_proxy_set_feature_ready (WP_PROXY (self),
         WP_ENDPOINT_STREAM_FEATURE_CONTROLS);
-    break;
   }
 }
 
@@ -224,26 +225,26 @@ get_name (WpEndpointStream * self)
   return priv->info->name;
 }
 
-static const struct spa_pod *
-get_control (WpEndpointStream * self, guint32 control_id)
+WpSpaPod *
+get_control (WpEndpointStream * self, const gchar * id_name)
 {
   WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-  return wp_spa_props_get_stored (&priv->spa_props, control_id);
+  return wp_spa_props_get_stored (&priv->spa_props, id_name);
 }
 
 static gboolean
-set_control (WpEndpointStream * self, guint32 control_id,
-    const struct spa_pod * pod)
+set_control (WpEndpointStream * self, const gchar * id_name,
+    const WpSpaPod * pod)
 {
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
+  g_autoptr (WpSpaPod) param = wp_spa_pod_new_object (
+      "Props", "Props",
+      id_name, "P", pod,
+      NULL);
 
   /* our spa_props will be updated by the param event */
 
   WP_PROXY_GET_CLASS (self)->set_param (WP_PROXY (self), SPA_PARAM_Props, 0,
-      spa_pod_builder_add_object (&b,
-          SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
-          control_id, SPA_POD_Pod (pod)));
+      param);
 
   return TRUE;
 }
@@ -282,7 +283,7 @@ wp_endpoint_stream_class_init (WpEndpointStreamClass * klass)
    */
   signals[SIGNAL_CONTROL_CHANGED] = g_signal_new (
       "control-changed", G_TYPE_FROM_CLASS (klass),
-      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
 }
 
 /**
@@ -303,146 +304,40 @@ wp_endpoint_stream_get_name (WpEndpointStream * self)
 /**
  * wp_endpoint_stream_get_control:
  * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
+ * @id_name: the control id (a #WpEndpointControl)
  *
- * Returns: (transfer none) (nullable): the `spa_pod` containing the value
+ * Returns: (transfer full) (nullable): the `spa_pod` containing the value
  *   of this control, or %NULL if @control_id does not exist on this endpoint
  *   stream
  */
-const struct spa_pod *
-wp_endpoint_stream_get_control (WpEndpointStream * self, guint32 control_id)
+WpSpaPod *
+wp_endpoint_stream_get_control (WpEndpointStream * self, const gchar * id_name)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT_STREAM (self), NULL);
   g_return_val_if_fail (WP_ENDPOINT_STREAM_GET_CLASS (self)->get_control, NULL);
 
-  return WP_ENDPOINT_STREAM_GET_CLASS (self)->get_control (self, control_id);
-}
-
-/**
- * wp_endpoint_stream_get_control_boolean:
- * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
- * @value: (out): the boolean value of this control
- *
- * Returns: %TRUE on success, %FALSE if the control does not exist on this
- *   endpoint stream or if it is not a boolean
- */
-gboolean
-wp_endpoint_stream_get_control_boolean (WpEndpointStream * self,
-    guint32 control_id, gboolean * value)
-{
-  const struct spa_pod *pod = wp_endpoint_stream_get_control (self, control_id);
-  bool val;
-  if (pod && spa_pod_get_bool (pod, &val) == 0) {
-    *value = val;
-    return TRUE;
-  }
-  return FALSE;
-}
-
-/**
- * wp_endpoint_stream_get_control_int:
- * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
- * @value: (out): the integer value of this control
- *
- * Returns: %TRUE on success, %FALSE if the control does not exist on this
- *   endpoint stream or if it is not an integer
- */
-gboolean
-wp_endpoint_stream_get_control_int (WpEndpointStream * self, guint32 control_id,
-    gint * value)
-{
-  const struct spa_pod *pod = wp_endpoint_stream_get_control (self, control_id);
-  return (pod && spa_pod_get_int (pod, value) == 0);
-}
-
-/**
- * wp_endpoint_stream_get_control_float:
- * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
- * @value: (out): the floating-point number value of this control
- *
- * Returns: %TRUE on success, %FALSE if the control does not exist on this
- *   endpoint stream or if it is not a floating-point number
- */
-gboolean
-wp_endpoint_stream_get_control_float (WpEndpointStream * self, guint32 control_id,
-    gfloat * value)
-{
-  const struct spa_pod *pod = wp_endpoint_stream_get_control (self, control_id);
-  return (pod && spa_pod_get_float (pod, value) == 0);
+  return WP_ENDPOINT_STREAM_GET_CLASS (self)->get_control (self, id_name);
 }
 
 /**
  * wp_endpoint_stream_set_control:
  * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
+ * @id_name: the control id (a #WpEndpointControl)
  * @value: the new value for this control, as a `spa_pod`
  *
  * Returns: %TRUE on success, %FALSE if an error occurred
  */
 gboolean
-wp_endpoint_stream_set_control (WpEndpointStream * self, guint32 control_id,
-    const struct spa_pod * value)
+wp_endpoint_stream_set_control (WpEndpointStream * self, const gchar * id_name,
+    const WpSpaPod * value)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT_STREAM (self), FALSE);
   g_return_val_if_fail (WP_ENDPOINT_STREAM_GET_CLASS (self)->set_control, FALSE);
 
-  return WP_ENDPOINT_STREAM_GET_CLASS (self)->set_control (self, control_id,
+  return WP_ENDPOINT_STREAM_GET_CLASS (self)->set_control (self, id_name,
       value);
 }
 
-/**
- * wp_endpoint_stream_set_control_boolean:
- * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
- * @value: the new value for this control, as a boolean
- *
- * Returns: %TRUE on success, %FALSE if an error occurred
- */
-gboolean
-wp_endpoint_stream_set_control_boolean (WpEndpointStream * self,
-    guint32 control_id, gboolean value)
-{
-  gchar buffer[512];
-  return wp_endpoint_stream_set_control (self, control_id,
-      wp_spa_props_build_pod (buffer, sizeof (buffer), SPA_POD_Bool (value), 0));
-}
-
-/**
- * wp_endpoint_stream_set_control_int:
- * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
- * @value: the new value for this control, as an integer
- *
- * Returns: %TRUE on success, %FALSE if an error occurred
- */
-gboolean
-wp_endpoint_stream_set_control_int (WpEndpointStream * self, guint32 control_id,
-    gint value)
-{
-  gchar buffer[512];
-  return wp_endpoint_stream_set_control (self, control_id,
-      wp_spa_props_build_pod (buffer, sizeof (buffer), SPA_POD_Int (value), 0));
-}
-
-/**
- * wp_endpoint_stream_set_control_float:
- * @self: the endpoint stream
- * @control_id: the control id (a #WpEndpointControl)
- * @value: the new value for this control, as a floating-point number
- *
- * Returns: %TRUE on success, %FALSE if an error occurred
- */
-gboolean
-wp_endpoint_stream_set_control_float (WpEndpointStream * self,
-    guint32 control_id, gfloat value)
-{
-  gchar buffer[512];
-  return wp_endpoint_stream_set_control (self, control_id,
-      wp_spa_props_build_pod (buffer, sizeof (buffer), SPA_POD_Float (value), 0));
-}
 
 /* WpImplEndpointStream */
 
@@ -515,10 +410,11 @@ impl_enum_params (void *object, int seq,
   switch (id) {
     case SPA_PARAM_PropInfo: {
       g_autoptr (GPtrArray) params =
-          wp_spa_props_build_propinfo (&priv->spa_props, &b);
+          wp_spa_props_build_propinfo (&priv->spa_props);
 
       for (guint i = start; i < params->len; i++) {
-        struct spa_pod *param = g_ptr_array_index (params, i);
+        WpSpaPod *pod = g_ptr_array_index (params, i);
+        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
 
         if (spa_pod_filter (&b, &result, param, filter) == 0) {
           pw_endpoint_stream_emit_param (&self->hooks, seq, id, i, i+1, result);
@@ -531,7 +427,8 @@ impl_enum_params (void *object, int seq,
     }
     case SPA_PARAM_Props: {
       if (start == 0) {
-        struct spa_pod *param = wp_spa_props_build_props (&priv->spa_props, &b);
+        g_autoptr (WpSpaPod) pod = wp_spa_props_build_props (&priv->spa_props);
+        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
         if (spa_pod_filter (&b, &result, param, filter) == 0) {
           pw_endpoint_stream_emit_param (&self->hooks, seq, id, 0, 1, result);
           wp_proxy_handle_event_param (self, seq, id, 0, 1, result);
@@ -566,13 +463,14 @@ impl_set_param (void *object, uint32_t id, uint32_t flags,
   WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
   WpEndpointStreamPrivate *priv =
       wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-  g_autoptr (GArray) changed_ids = NULL;
+  g_autoptr (GPtrArray) changed_ids = NULL;
 
   if (id != SPA_PARAM_Props)
     return -ENOENT;
 
-  changed_ids = g_array_new (FALSE, FALSE, sizeof (guint32));
-  wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
+  changed_ids = g_ptr_array_new_with_free_func (g_free);
+  g_autoptr (WpSpaPod) pod = wp_spa_pod_new_regular_wrap_copy (param);
+  wp_spa_props_store_from_props (&priv->spa_props, pod, changed_ids);
 
   /* notify subscribers */
   if (self->subscribed)
@@ -580,7 +478,7 @@ impl_set_param (void *object, uint32_t id, uint32_t flags,
 
   /* notify controls locally */
   for (guint i = 0; i < changed_ids->len; i++) {
-    guint32 prop_id = g_array_index (changed_ids, guint32, i);
+    const gchar * prop_id = g_ptr_array_index (changed_ids, i);
     g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, prop_id);
   }
 
diff --git a/lib/wp/endpoint-stream.h b/lib/wp/endpoint-stream.h
index f27a76cb..51e575e2 100644
--- a/lib/wp/endpoint-stream.h
+++ b/lib/wp/endpoint-stream.h
@@ -10,6 +10,7 @@
 #define __WIREPLUMBER_ENDPOINT_STREAM_H__
 
 #include "proxy.h"
+#include "spa-pod.h"
 
 G_BEGIN_DECLS
 
@@ -42,46 +43,21 @@ struct _WpEndpointStreamClass
 
   const gchar * (*get_name) (WpEndpointStream * self);
 
-  const struct spa_pod * (*get_control) (WpEndpointStream * self,
-      guint32 control_id);
-  gboolean (*set_control) (WpEndpointStream * self, guint32 control_id,
-      const struct spa_pod * value);
+  WpSpaPod * (*get_control) (WpEndpointStream * self, const gchar * id_name);
+  gboolean (*set_control) (WpEndpointStream * self, const gchar * id_name,
+      const WpSpaPod * value);
 };
 
 WP_API
 const gchar * wp_endpoint_stream_get_name (WpEndpointStream * self);
 
 WP_API
-const struct spa_pod * wp_endpoint_stream_get_control (WpEndpointStream * self,
-    guint32 control_id);
-
-WP_API
-gboolean wp_endpoint_stream_get_control_boolean (WpEndpointStream * self,
-    guint32 control_id, gboolean * value);
-
-WP_API
-gboolean wp_endpoint_stream_get_control_int (WpEndpointStream * self,
-    guint32 control_id, gint * value);
-
-WP_API
-gboolean wp_endpoint_stream_get_control_float (WpEndpointStream * self,
-    guint32 control_id, gfloat * value);
+WpSpaPod * wp_endpoint_stream_get_control (WpEndpointStream * self,
+    const gchar *id_name);
 
 WP_API
 gboolean wp_endpoint_stream_set_control (WpEndpointStream * self,
-    guint32 control_id, const struct spa_pod * value);
-
-WP_API
-gboolean wp_endpoint_stream_set_control_boolean (WpEndpointStream * self,
-    guint32 control_id, gboolean value);
-
-WP_API
-gboolean wp_endpoint_stream_set_control_int (WpEndpointStream * self,
-    guint32 control_id, gint value);
-
-WP_API
-gboolean wp_endpoint_stream_set_control_float (WpEndpointStream * self,
-    guint32 control_id, gfloat value);
+    const gchar *id_name, const WpSpaPod * value);
 
 G_END_DECLS
 
diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c
index 7a597c43..c6dc2342 100644
--- a/lib/wp/endpoint.c
+++ b/lib/wp/endpoint.c
@@ -172,14 +172,14 @@ wp_endpoint_get_properties (WpProxy * proxy)
 
 static gint
 wp_endpoint_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   WpEndpointPrivate *priv =
       wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   int endpoint_enum_params_result;
 
   endpoint_enum_params_result = pw_endpoint_enum_params (priv->iface, 0, id,
-      start, num, filter);
+      start, num, wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (endpoint_enum_params_result >= 0);
 
   return endpoint_enum_params_result;
@@ -201,14 +201,14 @@ wp_endpoint_subscribe_params (WpProxy * self, guint32 n_ids, guint32 *ids)
 
 static gint
 wp_endpoint_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   WpEndpointPrivate *priv =
       wp_endpoint_get_instance_private (WP_ENDPOINT (self));
   int endpoint_set_param_result;
 
   endpoint_set_param_result = pw_endpoint_set_param (priv->iface, id, flags,
-      param);
+      wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (endpoint_set_param_result >= 0);
 
   return endpoint_set_param_result;
@@ -262,30 +262,29 @@ wp_endpoint_bound (WpProxy * proxy, guint32 id)
 }
 
 static void
-wp_endpoint_param (WpProxy * proxy, gint seq, guint32 id, guint32 index,
-    guint32 next, const struct spa_pod *param)
+wp_endpoint_param (WpProxy * proxy, gint seq, const gchar * id_name,
+    guint32 index, guint32 next, const WpSpaPod *param)
 {
   WpEndpoint *self = WP_ENDPOINT (proxy);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-  g_autoptr (GArray) changed_ids = NULL;
-  guint32 prop_id;
+  g_autoptr (GPtrArray) changed_ids = NULL;
+  const gchar * prop_id;
 
-  switch (id) {
-  case SPA_PARAM_PropInfo:
+  if (g_strcmp0 ("PropInfo", id_name) == 0) {
     wp_spa_props_register_from_prop_info (&priv->spa_props, param);
-    break;
-  case SPA_PARAM_Props:
-    changed_ids = g_array_new (FALSE, FALSE, sizeof (uint32_t));
+  }
+
+  else if (g_strcmp0 ("Props", id_name) == 0) {
+    changed_ids = g_ptr_array_new_with_free_func (g_free);
     wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
 
     for (guint i = 0; i < changed_ids->len; i++) {
-      prop_id = g_array_index (changed_ids, uint32_t, i);
+      prop_id = g_ptr_array_index (changed_ids, i);
       g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, prop_id);
     }
 
     wp_proxy_set_feature_ready (WP_PROXY (self),
         WP_ENDPOINT_FEATURE_CONTROLS);
-    break;
   }
 }
 
@@ -310,26 +309,25 @@ get_direction (WpEndpoint * self)
   return priv->info->direction;
 }
 
-static const struct spa_pod *
-get_control (WpEndpoint * self, guint32 control_id)
+static WpSpaPod *
+get_control (WpEndpoint * self, const gchar * id_name)
 {
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-  return wp_spa_props_get_stored (&priv->spa_props, control_id);
+  return wp_spa_props_get_stored (&priv->spa_props, id_name);
 }
 
 static gboolean
-set_control (WpEndpoint * self, guint32 control_id,
-    const struct spa_pod * pod)
+set_control (WpEndpoint * self, const gchar * id_name, const WpSpaPod * pod)
 {
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
+  g_autoptr (WpSpaPod) param = wp_spa_pod_new_object (
+      "Props", "Props",
+      id_name, "P", pod,
+      NULL);
 
   /* our spa_props will be updated by the param event */
 
   WP_PROXY_GET_CLASS (self)->set_param (WP_PROXY (self), SPA_PARAM_Props, 0,
-      spa_pod_builder_add_object (&b,
-          SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
-          control_id, SPA_POD_Pod (pod)));
+      param);
 
   return TRUE;
 }
@@ -371,7 +369,7 @@ wp_endpoint_class_init (WpEndpointClass * klass)
    */
   signals[SIGNAL_CONTROL_CHANGED] = g_signal_new (
       "control-changed", G_TYPE_FROM_CLASS (klass),
-      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
 }
 
 /**
@@ -422,143 +420,36 @@ wp_endpoint_get_direction (WpEndpoint * self)
 /**
  * wp_endpoint_get_control:
  * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
+ * @id_name: the control id name
  *
- * Returns: (transfer none) (nullable): the `spa_pod` containing the value
+ * Returns: (transfer full) (nullable): the spa pod containing the value
  *   of this control, or %NULL if @control_id does not exist on this endpoint
  */
-const struct spa_pod *
-wp_endpoint_get_control (WpEndpoint * self, guint32 control_id)
+WpSpaPod *
+wp_endpoint_get_control (WpEndpoint * self, const gchar * id_name)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
   g_return_val_if_fail (WP_ENDPOINT_GET_CLASS (self)->get_control, NULL);
 
-  return WP_ENDPOINT_GET_CLASS (self)->get_control (self, control_id);
-}
-
-/**
- * wp_endpoint_get_control_boolean:
- * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
- * @value: (out): the boolean value of this control
- *
- * Returns: %TRUE on success, %FALSE if the control does not exist on this
- *   endpoint or if it is not a boolean
- */
-gboolean
-wp_endpoint_get_control_boolean (WpEndpoint * self, guint32 control_id,
-    gboolean * value)
-{
-  const struct spa_pod *pod = wp_endpoint_get_control (self, control_id);
-  bool val;
-  if (pod && spa_pod_get_bool (pod, &val) == 0) {
-    *value = val;
-    return TRUE;
-  }
-  return FALSE;
-}
-
-/**
- * wp_endpoint_get_control_int:
- * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
- * @value: (out): the integer value of this control
- *
- * Returns: %TRUE on success, %FALSE if the control does not exist on this
- *   endpoint or if it is not an integer
- */
-gboolean
-wp_endpoint_get_control_int (WpEndpoint * self, guint32 control_id,
-    gint * value)
-{
-  const struct spa_pod *pod = wp_endpoint_get_control (self, control_id);
-  return (pod && spa_pod_get_int (pod, value) == 0);
-}
-
-/**
- * wp_endpoint_get_control_float:
- * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
- * @value: (out): the floating-point number value of this control
- *
- * Returns: %TRUE on success, %FALSE if the control does not exist on this
- *   endpoint or if it is not a floating-point number
- */
-gboolean
-wp_endpoint_get_control_float (WpEndpoint * self, guint32 control_id,
-    gfloat * value)
-{
-  const struct spa_pod *pod = wp_endpoint_get_control (self, control_id);
-  return (pod && spa_pod_get_float (pod, value) == 0);
+  return WP_ENDPOINT_GET_CLASS (self)->get_control (self, id_name);
 }
 
 /**
  * wp_endpoint_set_control:
  * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
+ * @id_name: the control id name
  * @value: the new value for this control, as a `spa_pod`
  *
  * Returns: %TRUE on success, %FALSE if an error occurred
  */
 gboolean
-wp_endpoint_set_control (WpEndpoint * self, guint32 control_id,
-    const struct spa_pod * value)
+wp_endpoint_set_control (WpEndpoint * self, const gchar * id_name,
+    const WpSpaPod * value)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), FALSE);
   g_return_val_if_fail (WP_ENDPOINT_GET_CLASS (self)->set_control, FALSE);
 
-  return WP_ENDPOINT_GET_CLASS (self)->set_control (self, control_id, value);
-}
-
-/**
- * wp_endpoint_set_control_boolean:
- * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
- * @value: the new value for this control, as a boolean
- *
- * Returns: %TRUE on success, %FALSE if an error occurred
- */
-gboolean
-wp_endpoint_set_control_boolean (WpEndpoint * self, guint32 control_id,
-    gboolean value)
-{
-  gchar buffer[512];
-  return wp_endpoint_set_control (self, control_id, wp_spa_props_build_pod (
-          buffer, sizeof (buffer), SPA_POD_Bool (value), 0));
-}
-
-/**
- * wp_endpoint_set_control_int:
- * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
- * @value: the new value for this control, as an integer
- *
- * Returns: %TRUE on success, %FALSE if an error occurred
- */
-gboolean
-wp_endpoint_set_control_int (WpEndpoint * self, guint32 control_id,
-    gint value)
-{
-  gchar buffer[512];
-  return wp_endpoint_set_control (self, control_id, wp_spa_props_build_pod (
-          buffer, sizeof (buffer), SPA_POD_Int (value), 0));
-}
-
-/**
- * wp_endpoint_set_control_float:
- * @self: the endpoint
- * @control_id: the control id (a #WpEndpointControl)
- * @value: the new value for this control, as a floating-point number
- *
- * Returns: %TRUE on success, %FALSE if an error occurred
- */
-gboolean
-wp_endpoint_set_control_float (WpEndpoint * self, guint32 control_id,
-    gfloat value)
-{
-  gchar buffer[512];
-  return wp_endpoint_set_control (self, control_id, wp_spa_props_build_pod (
-          buffer, sizeof (buffer), SPA_POD_Float (value), 0));
+  return WP_ENDPOINT_GET_CLASS (self)->set_control (self, id_name, value);
 }
 
 /**
@@ -684,10 +575,11 @@ impl_enum_params (void *object, int seq,
   switch (id) {
     case SPA_PARAM_PropInfo: {
       g_autoptr (GPtrArray) params =
-          wp_spa_props_build_propinfo (&priv->spa_props, &b);
+          wp_spa_props_build_propinfo (&priv->spa_props);
 
       for (guint i = start; i < params->len; i++) {
-        struct spa_pod *param = g_ptr_array_index (params, i);
+        WpSpaPod *pod = g_ptr_array_index (params, i);
+        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
 
         if (spa_pod_filter (&b, &result, param, filter) == 0) {
           pw_endpoint_emit_param (&self->hooks, seq, id, i, i+1, result);
@@ -700,7 +592,8 @@ impl_enum_params (void *object, int seq,
     }
     case SPA_PARAM_Props: {
       if (start == 0) {
-        struct spa_pod *param = wp_spa_props_build_props (&priv->spa_props, &b);
+        g_autoptr (WpSpaPod) pod = wp_spa_props_build_props (&priv->spa_props);
+        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
         if (spa_pod_filter (&b, &result, param, filter) == 0) {
           pw_endpoint_emit_param (&self->hooks, seq, id, 0, 1, result);
           wp_proxy_handle_event_param (self, seq, id, 0, 1, result);
@@ -735,13 +628,14 @@ impl_set_param (void *object, uint32_t id, uint32_t flags,
   WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
   WpEndpointPrivate *priv =
       wp_endpoint_get_instance_private (WP_ENDPOINT (self));
-  g_autoptr (GArray) changed_ids = NULL;
+  g_autoptr (GPtrArray) changed_ids = NULL;
 
   if (id != SPA_PARAM_Props)
     return -ENOENT;
 
-  changed_ids = g_array_new (FALSE, FALSE, sizeof (guint32));
-  wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
+  changed_ids = g_ptr_array_new_with_free_func (g_free);
+  g_autoptr (WpSpaPod) pod = wp_spa_pod_new_regular_wrap_copy (param);
+  wp_spa_props_store_from_props (&priv->spa_props, pod, changed_ids);
 
   /* notify subscribers */
   if (self->subscribed)
@@ -749,7 +643,7 @@ impl_set_param (void *object, uint32_t id, uint32_t flags,
 
   /* notify controls locally */
   for (guint i = 0; i < changed_ids->len; i++) {
-    guint32 prop_id = g_array_index (changed_ids, guint32, i);
+    const gchar * prop_id = g_ptr_array_index (changed_ids, i);
     g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, prop_id);
   }
 
@@ -1158,7 +1052,7 @@ wp_impl_endpoint_new (WpCore * core, WpSiEndpoint * item)
 /**
  * wp_impl_endpoint_register_control:
  * @self: the endpoint implementation
- * @control: the control to make available
+ * @id_name: the control id name to make available
  *
  * Registers the specified @control as a SPA property of this endpoint,
  * making it appear to remote clients.
@@ -1168,29 +1062,24 @@ wp_impl_endpoint_new (WpCore * core, WpSiEndpoint * item)
  */
 void
 wp_impl_endpoint_register_control (WpImplEndpoint * self,
-    WpEndpointControl control)
+    const gchar *id_name)
 {
   WpImplEndpointPrivate *priv;
 
   g_return_if_fail (WP_IS_IMPL_ENDPOINT (self));
   priv = wp_impl_endpoint_get_instance_private (self);
 
-  switch (control) {
-  case WP_ENDPOINT_CONTROL_VOLUME:
-    wp_spa_props_register (&priv->pp->spa_props, control,
-      "Volume", SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0));
-    break;
-  case WP_ENDPOINT_CONTROL_MUTE:
-    wp_spa_props_register (&priv->pp->spa_props, control,
-      "Mute", SPA_POD_CHOICE_Bool (false));
-    break;
-  case WP_ENDPOINT_CONTROL_CHANNEL_VOLUMES:
-    wp_spa_props_register (&priv->pp->spa_props, control,
-      "Channel Volumes", SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0));
-    break;
-  default:
-    g_warning ("Unknown endpoint control: 0x%x", control);
-    break;
+  if (g_strcmp0 ("volume", id_name) == 0) {
+    wp_spa_props_register (&priv->pp->spa_props, id_name, "Volume",
+        wp_spa_pod_new_choice ("Range", "f", 1.0, "f", 0.0, "f", 10.0, NULL));
+  } else if (g_strcmp0 ("mute", id_name) == 0) {
+    wp_spa_props_register (&priv->pp->spa_props, id_name, "Mute",
+        wp_spa_pod_new_boolean (FALSE));
+  } else if (g_strcmp0 ("channelVolumes", id_name) == 0) {
+    wp_spa_props_register (&priv->pp->spa_props, id_name, "Channel Volumes",
+        wp_spa_pod_new_choice ("Range", "f", 1.0, "f", 0.0, "f", 10.0, NULL));
+  } else {
+    g_warning ("Unknown endpoint control: %s", id_name);
   }
 }
 #endif
diff --git a/lib/wp/endpoint.h b/lib/wp/endpoint.h
index 4e75d785..7bd15a18 100644
--- a/lib/wp/endpoint.h
+++ b/lib/wp/endpoint.h
@@ -9,6 +9,7 @@
 #ifndef __WIREPLUMBER_ENDPOINT_H__
 #define __WIREPLUMBER_ENDPOINT_H__
 
+#include "spa-pod.h"
 #include "proxy.h"
 #include "endpoint-stream.h"
 #include "iterator.h"
@@ -27,18 +28,6 @@ typedef enum {
   WP_DIRECTION_OUTPUT,
 } WpDirection;
 
-/**
- * WpEndpointControl:
- * @WP_ENDPOINT_CONTROL_VOLUME: a volume control (type: float)
- * @WP_ENDPOINT_CONTROL_MUTE: a mute control (type: boolean)
- * @WP_ENDPOINT_CONTROL_CHANNEL_VOLUMES:
- */
-typedef enum {
-  WP_ENDPOINT_CONTROL_VOLUME = 0x10003 /* SPA_PROP_volume */,
-  WP_ENDPOINT_CONTROL_MUTE = 0x10004 /* SPA_PROP_mute */,
-  WP_ENDPOINT_CONTROL_CHANNEL_VOLUMES = 0x10008 /* SPA_PROP_channelVolumes */,
-} WpEndpointControl;
-
 /**
  * WpEndpointFeatures:
  * @WP_ENDPOINT_FEATURE_CONTROLS: enables the use of the
@@ -83,9 +72,9 @@ struct _WpEndpointClass
   const gchar * (*get_media_class) (WpEndpoint * self);
   WpDirection (*get_direction) (WpEndpoint * self);
 
-  const struct spa_pod * (*get_control) (WpEndpoint * self, guint32 control_id);
-  gboolean (*set_control) (WpEndpoint * self, guint32 control_id,
-      const struct spa_pod * value);
+  WpSpaPod * (*get_control) (WpEndpoint * self, const gchar * id_name);
+  gboolean (*set_control) (WpEndpoint * self, const gchar * id_name,
+      const WpSpaPod * value);
 };
 
 WP_API
@@ -98,36 +87,11 @@ WP_API
 WpDirection wp_endpoint_get_direction (WpEndpoint * self);
 
 WP_API
-const struct spa_pod * wp_endpoint_get_control (WpEndpoint * self,
-    guint32 control_id);
-
-WP_API
-gboolean wp_endpoint_get_control_boolean (WpEndpoint * self, guint32 control_id,
-    gboolean * value);
-
-WP_API
-gboolean wp_endpoint_get_control_int (WpEndpoint * self, guint32 control_id,
-    gint * value);
-
-WP_API
-gboolean wp_endpoint_get_control_float (WpEndpoint * self, guint32 control_id,
-    gfloat * value);
-
-WP_API
-gboolean wp_endpoint_set_control (WpEndpoint * self, guint32 control_id,
-    const struct spa_pod * value);
-
-WP_API
-gboolean wp_endpoint_set_control_boolean (WpEndpoint * self, guint32 control_id,
-    gboolean value);
-
-WP_API
-gboolean wp_endpoint_set_control_int (WpEndpoint * self, guint32 control_id,
-    gint value);
+WpSpaPod * wp_endpoint_get_control (WpEndpoint * self, const gchar * id_name);
 
 WP_API
-gboolean wp_endpoint_set_control_float (WpEndpoint * self, guint32 control_id,
-    gfloat value);
+gboolean wp_endpoint_set_control (WpEndpoint * self, const gchar * id_name,
+    const WpSpaPod * value);
 
 WP_API
 guint wp_endpoint_get_n_streams (WpEndpoint * self);
diff --git a/lib/wp/node.c b/lib/wp/node.c
index 1fc286bd..93946eb5 100644
--- a/lib/wp/node.c
+++ b/lib/wp/node.c
@@ -76,13 +76,14 @@ wp_node_get_properties (WpProxy * self)
 
 static gint
 wp_node_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   struct pw_node *pwp;
   int node_enum_params_result;
 
   pwp = (struct pw_node *) wp_proxy_get_pw_proxy (self);
-  node_enum_params_result = pw_node_enum_params (pwp, 0, id, start, num, filter);
+  node_enum_params_result = pw_node_enum_params (pwp, 0, id, start, num,
+      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
   g_warn_if_fail (node_enum_params_result >= 0);
 
   return node_enum_params_result;
@@ -103,13 +104,14 @@ wp_node_subscribe_params (WpProxy * self, guint32 n_ids, guint32 *ids)
 
 static gint
 wp_node_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   struct pw_node *pwp;
   int node_set_param_result;
 
   pwp = (struct pw_node *) wp_proxy_get_pw_proxy (self);
-  node_set_param_result = pw_node_set_param (pwp, id, flags, param);
+  node_set_param_result = pw_node_set_param (pwp, id, flags,
+      wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (node_set_param_result >= 0);
 
   return node_set_param_result;
diff --git a/lib/wp/port.c b/lib/wp/port.c
index 922bbac4..735e7337 100644
--- a/lib/wp/port.c
+++ b/lib/wp/port.c
@@ -66,13 +66,14 @@ wp_port_get_properties (WpProxy * self)
 
 static gint
 wp_port_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   struct pw_port *pwp;
   int port_enum_params_result;
 
   pwp = (struct pw_port *) wp_proxy_get_pw_proxy (self);
-  port_enum_params_result = pw_port_enum_params (pwp, 0, id, start, num, filter);
+  port_enum_params_result = pw_port_enum_params (pwp, 0, id, start, num,
+      wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (port_enum_params_result >= 0);
 
   return port_enum_params_result;
diff --git a/lib/wp/private.h b/lib/wp/private.h
index b064f5ea..3934b468 100644
--- a/lib/wp/private.h
+++ b/lib/wp/private.h
@@ -149,7 +149,7 @@ WpSpaPod * wp_spa_pod_new_property_wrap_copy (WpSpaTypeTable table, guint32 key,
     guint32 flags, const struct spa_pod *pod);
 WpSpaPod * wp_spa_pod_new_control_wrap_copy (guint32 offset, guint32 type,
     const struct spa_pod *pod);
-struct spa_pod *wp_spa_pod_get_spa_pod (WpSpaPod *self);
+const struct spa_pod *wp_spa_pod_get_spa_pod (const WpSpaPod *self);
 
 /* spa props */
 
@@ -160,54 +160,22 @@ struct _WpSpaProps
 
 void wp_spa_props_clear (WpSpaProps * self);
 
-void wp_spa_props_register_pod (WpSpaProps * self,
-    guint32 id, const gchar *name, const struct spa_pod *type);
-gint wp_spa_props_register_from_prop_info (WpSpaProps * self,
-    const struct spa_pod * prop_info);
+void wp_spa_props_register (WpSpaProps * self,
+    const char *id_name, const gchar *description, WpSpaPod *type);
+gboolean wp_spa_props_register_from_prop_info (WpSpaProps * self,
+    const WpSpaPod * prop_info);
 
-const struct spa_pod * wp_spa_props_get_stored (WpSpaProps * self, guint32 id);
+WpSpaPod *
+wp_spa_props_get_stored (WpSpaProps * self, const char * id_name);
 
-gint wp_spa_props_store_pod (WpSpaProps * self, guint32 id,
-    const struct spa_pod * value);
-gint wp_spa_props_store_from_props (WpSpaProps * self,
-    const struct spa_pod * props, GArray * changed_ids);
+gboolean wp_spa_props_store (WpSpaProps * self, const char *id_name,
+    const WpSpaPod * value);
+gboolean wp_spa_props_store_from_props (WpSpaProps * self,
+    const WpSpaPod * props, GPtrArray * changed_ids);
 
-struct spa_pod * wp_spa_props_build_props (WpSpaProps * self,
-    struct spa_pod_builder * b);
-GPtrArray * wp_spa_props_build_propinfo (WpSpaProps * self,
-    struct spa_pod_builder * b);
-GPtrArray * wp_spa_props_build_all_pods (WpSpaProps * self,
-    struct spa_pod_builder * b);
-struct spa_pod * wp_spa_props_build_update (WpSpaProps * self, guint32 id,
-    const struct spa_pod * value, struct spa_pod_builder * b);
-
-const struct spa_pod * wp_spa_props_build_pod_valist (gchar * buffer,
-    gsize size, va_list args);
-
-static inline const struct spa_pod *
-wp_spa_props_build_pod (gchar * buffer, gsize size, ...)
-{
-  const struct spa_pod *ret;
-  va_list args;
-  va_start (args, size);
-  ret = wp_spa_props_build_pod_valist (buffer, size, args);
-  va_end (args);
-  return ret;
-}
-
-#define wp_spa_props_register(self, id, name, ...) \
-({ \
-  gchar b[512]; \
-  wp_spa_props_register_pod (self, id, name, \
-      wp_spa_props_build_pod (b, sizeof (b), ##__VA_ARGS__, NULL)); \
-})
-
-#define wp_spa_props_store(self, id, ...) \
-({ \
-  gchar b[512]; \
-  wp_spa_props_store_pod (self, id, \
-      wp_spa_props_build_pod (b, sizeof (b), ##__VA_ARGS__, NULL)); \
-})
+WpSpaPod * wp_spa_props_build_props (WpSpaProps * self);
+GPtrArray * wp_spa_props_build_propinfo (WpSpaProps * self);
+GPtrArray * wp_spa_props_build_all_pods (WpSpaProps * self);
 
 /* impl endpoint */
 
diff --git a/lib/wp/proxy.c b/lib/wp/proxy.c
index 414153ec..820d8a7c 100644
--- a/lib/wp/proxy.c
+++ b/lib/wp/proxy.c
@@ -368,7 +368,7 @@ wp_proxy_class_init (WpProxyClass * klass)
   wp_proxy_signals[SIGNAL_PARAM] = g_signal_new (
       "param", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
       G_STRUCT_OFFSET (WpProxyClass, param), NULL, NULL, NULL, G_TYPE_NONE, 5,
-      G_TYPE_INT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_POINTER);
+      G_TYPE_INT, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, WP_TYPE_SPA_POD);
 }
 
 void
@@ -667,7 +667,7 @@ wp_proxy_find_async_task (WpProxy * self, int seq, gboolean steal)
  */
 gint
 wp_proxy_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   g_return_val_if_fail (WP_IS_PROXY (self), -EINVAL);
 
@@ -721,7 +721,7 @@ enum_params_done (WpCore * core, GAsyncResult * res, gpointer data)
  */
 void
 wp_proxy_enum_params_collect (WpProxy * self,
-    guint32 id, guint32 start, guint32 num, const struct spa_pod *filter,
+    guint32 id, guint32 start, guint32 num, const WpSpaPod * filter,
     GCancellable * cancellable, GAsyncReadyCallback callback,
     gpointer user_data)
 {
@@ -733,7 +733,7 @@ wp_proxy_enum_params_collect (WpProxy * self,
 
   /* create task for enum_params */
   task = g_task_new (self, cancellable, callback, user_data);
-  params = g_ptr_array_new_with_free_func (free);
+  params = g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
   g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
 
   /* call enum_params */
@@ -756,7 +756,7 @@ wp_proxy_enum_params_collect (WpProxy * self,
 /**
  * wp_proxy_enum_params_collect_finish:
  *
- * Returns: (transfer full) (element-type spa_pod*):
+ * Returns: (transfer full) (element-type WpSpaPod*):
  *    the collected params
  */
 GPtrArray *
@@ -847,7 +847,7 @@ wp_proxy_subscribe_params_array (WpProxy * self, guint32 n_ids, guint32 *ids)
  */
 gint
 wp_proxy_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   g_return_val_if_fail (WP_IS_PROXY (self), -EINVAL);
 
@@ -866,14 +866,19 @@ wp_proxy_handle_event_param (void * proxy, int seq, uint32_t id,
   WpProxy *self = WP_PROXY (proxy);
   GTask *task;
 
-  g_signal_emit (self, wp_proxy_signals[SIGNAL_PARAM], 0, seq, id, index, next,
-      param);
+  const gchar *id_name = NULL;
+  wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, id, NULL, &id_name, NULL);
+  g_return_if_fail (id_name);
+
+  g_autoptr (WpSpaPod) pod = wp_spa_pod_new_regular_wrap_copy (param);
+  g_signal_emit (self, wp_proxy_signals[SIGNAL_PARAM], 0, seq, id_name, index,
+      next, pod);
 
   /* if this param event was emited because of enum_params_collect(),
    * copy the param in the result array of that API */
   task = wp_proxy_find_async_task (self, seq, FALSE);
   if (task) {
     GPtrArray *array = g_task_get_task_data (task);
-    g_ptr_array_add (array, spa_pod_copy (param));
+    g_ptr_array_add (array, g_steal_pointer (&pod));
   }
 }
diff --git a/lib/wp/proxy.h b/lib/wp/proxy.h
index 9144dddc..4808a7f9 100644
--- a/lib/wp/proxy.h
+++ b/lib/wp/proxy.h
@@ -12,12 +12,12 @@
 
 #include <gio/gio.h>
 
+#include "spa-pod.h"
 #include "properties.h"
 
 G_BEGIN_DECLS
 
 struct pw_proxy;
-struct spa_pod;
 typedef struct _WpCore WpCore;
 
 /**
@@ -79,18 +79,18 @@ struct _WpProxyClass
   WpProperties * (*get_properties) (WpProxy * self);
 
   gint (*enum_params) (WpProxy * self, guint32 id, guint32 start, guint32 num,
-      const struct spa_pod * filter);
+      const WpSpaPod * filter);
   gint (*subscribe_params) (WpProxy * self, guint32 n_ids, guint32 *ids);
   gint (*set_param) (WpProxy * self, guint32 id, guint32 flags,
-      const struct spa_pod * param);
+      const WpSpaPod * param);
 
   /* signals */
 
   void (*pw_proxy_created) (WpProxy * self, struct pw_proxy * proxy);
   void (*pw_proxy_destroyed) (WpProxy * self);
   void (*bound) (WpProxy * self, guint32 id);
-  void (*param) (WpProxy * self, gint seq, guint32 id, guint32 index,
-      guint32 next, const struct spa_pod *param);
+  void (*param) (WpProxy * self, gint seq, const gchar * id_name, guint32 index,
+      guint32 next, const WpSpaPod *param);
 };
 
 /* features API */
@@ -142,11 +142,11 @@ guint32 wp_proxy_get_bound_id (WpProxy * self);
 
 WP_API
 gint wp_proxy_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter);
+    guint32 num, const WpSpaPod * filter);
 
 WP_API
 void wp_proxy_enum_params_collect (WpProxy * self,
-    guint32 id, guint32 start, guint32 num, const struct spa_pod *filter,
+    guint32 id, guint32 start, guint32 num, const WpSpaPod *filter,
     GCancellable * cancellable, GAsyncReadyCallback callback,
     gpointer user_data);
 
@@ -163,7 +163,7 @@ gint wp_proxy_subscribe_params_array (WpProxy * self, guint32 n_ids,
 
 WP_API
 gint wp_proxy_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param);
+    const WpSpaPod *param);
 
 G_END_DECLS
 
diff --git a/lib/wp/session.c b/lib/wp/session.c
index c87fcc4f..baac77a4 100644
--- a/lib/wp/session.c
+++ b/lib/wp/session.c
@@ -22,6 +22,8 @@
 
 #define G_LOG_DOMAIN "wp-session"
 
+#include "spa-type.h"
+#include "spa-pod.h"
 #include "session.h"
 #include "private.h"
 #include "error.h"
@@ -98,13 +100,13 @@ wp_session_get_properties (WpProxy * proxy)
 
 static gint
 wp_session_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, const struct spa_pod *filter)
+    guint32 num, const WpSpaPod * filter)
 {
   WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
   int session_enum_params_result;
 
   session_enum_params_result = pw_session_enum_params (priv->iface, 0, id,
-      start, num, filter);
+      start, num, wp_spa_pod_get_spa_pod (filter));
   g_warn_if_fail (session_enum_params_result >= 0);
 
   return session_enum_params_result;
@@ -125,13 +127,13 @@ wp_session_subscribe_params (WpProxy * self, guint32 n_ids, guint32 *ids)
 
 static gint
 wp_session_set_param (WpProxy * self, guint32 id, guint32 flags,
-    const struct spa_pod *param)
+    const WpSpaPod *param)
 {
   WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
   int session_set_param_result;
 
   session_set_param_result = pw_session_set_param (priv->iface, id, flags,
-      param);
+      wp_spa_pod_get_spa_pod (param));
   g_warn_if_fail (session_set_param_result >= 0);
 
   return session_set_param_result;
@@ -174,35 +176,34 @@ wp_session_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 }
 
 static void
-wp_session_param (WpProxy * proxy, gint seq, guint32 id, guint32 index,
-    guint32 next, const struct spa_pod *param)
+wp_session_param (WpProxy * proxy, gint seq, const gchar * id_name,
+    guint32 index, guint32 next, const WpSpaPod *param)
 {
   WpSession *self = WP_SESSION (proxy);
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
-  g_autoptr (GArray) changed_ids = NULL;
-  guint32 prop_id;
+  g_autoptr (GPtrArray) changed_ids = NULL;
+  g_autoptr (WpSpaPod) stored = NULL;
+  const gchar * prop_id;
   gint32 value;
 
-  switch (id) {
-  case SPA_PARAM_PropInfo:
+  if (g_strcmp0 ("PropInfo", id_name) == 0) {
     wp_spa_props_register_from_prop_info (&priv->spa_props, param);
-    break;
-  case SPA_PARAM_Props:
-    changed_ids = g_array_new (FALSE, FALSE, sizeof (uint32_t));
+  }
+
+  else if (g_strcmp0 ("Props", id_name) == 0) {
+    changed_ids = g_ptr_array_new_with_free_func (g_free);
     wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
 
     for (guint i = 0; i < changed_ids->len; i++) {
-      prop_id = g_array_index (changed_ids, uint32_t, i);
-      param = wp_spa_props_get_stored (&priv->spa_props, prop_id);
-      if (spa_pod_get_int (param, &value) == 0) {
-        g_signal_emit (self, signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED], 0,
-            prop_id, value);
-      }
+      prop_id = g_ptr_array_index (changed_ids, i);
+      stored = wp_spa_props_get_stored (&priv->spa_props, prop_id);
+      wp_spa_pod_get_int (stored, &value);
+      g_signal_emit (self, signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED], 0, prop_id,
+          value);
     }
 
     wp_proxy_set_feature_ready (WP_PROXY (self),
         WP_SESSION_FEATURE_DEFAULT_ENDPOINT);
-    break;
   }
 }
 
@@ -348,31 +349,31 @@ wp_session_augment (WpProxy * proxy, WpProxyFeatures features)
 }
 
 static guint32
-get_default_endpoint (WpSession * self, WpDefaultEndpointType type)
+get_default_endpoint (WpSession * self, const gchar * type_name)
 {
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
-  const struct spa_pod *pod;
+  g_autoptr (WpSpaPod) pod = NULL;
   gint32 value;
 
-  pod = wp_spa_props_get_stored (&priv->spa_props, type);
-  if (pod && spa_pod_get_int (pod, &value) == 0)
+  pod = wp_spa_props_get_stored (&priv->spa_props, type_name);
+  if (pod && wp_spa_pod_get_int (pod, &value))
     return (guint32) value;
   return 0;
 }
 
 static void
-set_default_endpoint (WpSession * self, WpDefaultEndpointType type, guint32 id)
+set_default_endpoint (WpSession * self, const gchar * type_name, guint32 id)
 {
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
+  g_autoptr (WpSpaPod) param = wp_spa_pod_new_object (
+      "Props", "Props",
+      type_name, "i", id,
+      NULL);
 
   /* set the default endpoint id as a property param on the session;
      our spa_props cache will be updated by the param event */
 
   WP_PROXY_GET_CLASS (self)->set_param (WP_PROXY (self), SPA_PARAM_Props, 0,
-      spa_pod_builder_add_object (&b,
-          SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
-          type, SPA_POD_Int (id)));
+      param);
 }
 
 static void
@@ -381,6 +382,17 @@ wp_session_class_init (WpSessionClass * klass)
   GObjectClass *object_class = (GObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
+  /* Register custom wireplumber session types */
+  wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS,
+      "Wp:Session:Default:Endpoint:Audio:Source",
+      "wp-session-default-endpoint-audio-source");
+  wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS,
+      "Wp:Session:Default:Endpoint:Audio:Sink",
+      "wp-session-default-endpoint-audio-sink");
+  wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS,
+      "Wp:Session:Default:Endpoint:Video:Source",
+      "wp-session-default-endpoint-video-source");
+
   object_class->finalize = wp_session_finalize;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Session;
@@ -413,43 +425,43 @@ wp_session_class_init (WpSessionClass * klass)
   signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED] = g_signal_new (
       "default-endpoint-changed", G_TYPE_FROM_CLASS (klass),
       G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
-      WP_TYPE_DEFAULT_ENDPOINT_TYPE, G_TYPE_UINT);
+      G_TYPE_STRING, G_TYPE_UINT);
 }
 
 /**
  * wp_session_get_default_endpoint:
  * @self: the session
- * @type: the endpoint type
+ * @type_name: the endpoint type name
  *
  * Returns: the bound id of the default endpoint of this @type
  */
 guint32
 wp_session_get_default_endpoint (WpSession * self,
-    WpDefaultEndpointType type)
+    const gchar * type_name)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), SPA_ID_INVALID);
   g_return_val_if_fail (WP_SESSION_GET_CLASS (self)->get_default_endpoint,
       SPA_ID_INVALID);
 
-  return WP_SESSION_GET_CLASS (self)->get_default_endpoint (self, type);
+  return WP_SESSION_GET_CLASS (self)->get_default_endpoint (self, type_name);
 }
 
 /**
  * wp_session_set_default_endpoint:
  * @self: the session
- * @type: the endpoint type
+ * @type_name: the endpoint type name
  * @id: the bound id of the endpoint to set as the default for this @type
  *
  * Sets the default endpoint for this @type to be the one identified with @id
  */
 void
-wp_session_set_default_endpoint (WpSession * self,
-    WpDefaultEndpointType type, guint32 id)
+wp_session_set_default_endpoint (WpSession * self, const char * type_name,
+    guint32 id)
 {
   g_return_if_fail (WP_IS_SESSION (self));
   g_return_if_fail (WP_SESSION_GET_CLASS (self)->set_default_endpoint);
 
-  WP_SESSION_GET_CLASS (self)->set_default_endpoint (self, type, id);
+  WP_SESSION_GET_CLASS (self)->set_default_endpoint (self, type_name, id);
 }
 
 /**
@@ -624,10 +636,11 @@ impl_enum_params (void *object, int seq,
   switch (id) {
     case SPA_PARAM_PropInfo: {
       g_autoptr (GPtrArray) params =
-          wp_spa_props_build_propinfo (&priv->spa_props, &b);
+          wp_spa_props_build_propinfo (&priv->spa_props);
 
       for (guint i = start; i < params->len; i++) {
-        struct spa_pod *param = g_ptr_array_index (params, i);
+        WpSpaPod *pod = g_ptr_array_index (params, i);
+        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
         pw_session_emit_param (&self->hooks, seq, id, i, i+1, param);
         wp_proxy_handle_event_param (self, seq, id, i, i+1, param);
         if (++count == num)
@@ -637,7 +650,8 @@ impl_enum_params (void *object, int seq,
     }
     case SPA_PARAM_Props: {
       if (start == 0) {
-        struct spa_pod *param = wp_spa_props_build_props (&priv->spa_props, &b);
+        g_autoptr (WpSpaPod) pod = wp_spa_props_build_props (&priv->spa_props);
+        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
         if (spa_pod_filter (&b, &result, param, filter) == 0) {
           pw_session_emit_param (&self->hooks, seq, id, 0, 1, result);
           wp_proxy_handle_event_param (self, seq, id, 0, 1, result);
@@ -656,7 +670,6 @@ static int
 impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
 {
   WpImplSession *self = WP_IMPL_SESSION (object);
-
   for (guint i = 0; i < n_ids; i++) {
     if (ids[i] == SPA_PARAM_Props)
       self->subscribed = TRUE;
@@ -670,15 +683,15 @@ impl_set_param (void *object, uint32_t id, uint32_t flags,
     const struct spa_pod *param)
 {
   WpImplSession *self = WP_IMPL_SESSION (object);
-  WpSessionPrivate *priv =
-      wp_session_get_instance_private (WP_SESSION (self));
-  g_autoptr (GArray) changed_ids = NULL;
+  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
+  g_autoptr (GPtrArray) changed_ids = NULL;
 
   if (id != SPA_PARAM_Props)
     return -ENOENT;
 
-  changed_ids = g_array_new (FALSE, FALSE, sizeof (guint32));
-  wp_spa_props_store_from_props (&priv->spa_props, param, changed_ids);
+  changed_ids = g_ptr_array_new_with_free_func (g_free);
+  g_autoptr (WpSpaPod) pod = wp_spa_pod_new_regular_wrap_copy (param);
+  wp_spa_props_store_from_props (&priv->spa_props, pod, changed_ids);
 
   /* notify subscribers */
   if (self->subscribed)
@@ -686,15 +699,13 @@ impl_set_param (void *object, uint32_t id, uint32_t flags,
 
   /* notify controls locally */
   for (guint i = 0; i < changed_ids->len; i++) {
-    guint32 prop_id = g_array_index (changed_ids, guint32, i);
-    const struct spa_pod *param =
-        wp_spa_props_get_stored (&priv->spa_props, prop_id);
+    const gchar * prop_id = g_ptr_array_index (changed_ids, i);
+    g_autoptr (WpSpaPod) pod = wp_spa_props_get_stored (&priv->spa_props,
+        prop_id);
     gint value;
-
-    if (spa_pod_get_int (param, &value) == 0) {
-      g_signal_emit (self, signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED], 0,
-          prop_id, value);
-    }
+    if (wp_spa_pod_get_int (pod, &value))
+      g_signal_emit (self, signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED], 0, prop_id,
+        value);
   }
 
   return 0;
@@ -736,16 +747,16 @@ wp_impl_session_init (WpImplSession * self)
 
   wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
 
-  /* prepare DEFAULT_ENDPOINT */
+  /* prepare default endpoint */
   wp_spa_props_register (&priv->spa_props,
-      WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE,
-      "Default Audio Source", SPA_POD_Int (0));
+      "wp-session-default-endpoint-audio-source",
+      "Default Audio Source", wp_spa_pod_new_int (0));
   wp_spa_props_register (&priv->spa_props,
-      WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK,
-      "Default Audio Sink", SPA_POD_Int (0));
+      "wp-session-default-endpoint-audio-sink",
+      "Default Audio Sink", wp_spa_pod_new_int (0));
   wp_spa_props_register (&priv->spa_props,
-      WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE,
-      "Default Video Source", SPA_POD_Int (0));
+      "wp-session-default-endpoint-video-source",
+      "Default Video Source", wp_spa_pod_new_int (0));
 
   wp_proxy_set_feature_ready (WP_PROXY (self),
       WP_SESSION_FEATURE_DEFAULT_ENDPOINT);
diff --git a/lib/wp/session.h b/lib/wp/session.h
index 2bcb7dc8..d501105a 100644
--- a/lib/wp/session.h
+++ b/lib/wp/session.h
@@ -15,20 +15,6 @@
 
 G_BEGIN_DECLS
 
-/**
- * WpDefaultEndpointType:
- * @WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE: the default audio source (capture)
- *    endpoint
- * @WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK: the default audio sink (playback)
- *    endpoint
- * @WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE: the default video source endpoint
- */
-typedef enum {
-  WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE = 0x1000000 /* SPA_PROP_START_CUSTOM */,
-  WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK,
-  WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE,
-} WpDefaultEndpointType;
-
 /**
  * WpSessionFeatures:
  * @WP_SESSION_FEATURE_DEFAULT_ENDPOINT: enables the use of
@@ -74,19 +60,18 @@ struct _WpSessionClass
 {
   WpProxyClass parent_class;
 
-  guint32 (*get_default_endpoint) (WpSession * self,
-      WpDefaultEndpointType type);
-  void (*set_default_endpoint) (WpSession * self,
-      WpDefaultEndpointType type, guint32 id);
+  guint32 (*get_default_endpoint) (WpSession * self, const gchar * type_name);
+  void (*set_default_endpoint) (WpSession * self, const gchar * type_name,
+      guint32 id);
 };
 
 WP_API
 guint32 wp_session_get_default_endpoint (WpSession * self,
-    WpDefaultEndpointType type);
+    const gchar * type_name);
 
 WP_API
-void wp_session_set_default_endpoint (WpSession * self,
-    WpDefaultEndpointType type, guint32 id);
+void wp_session_set_default_endpoint (WpSession * self, const gchar * type_name,
+    guint32 id);
 
 WP_API
 guint wp_session_get_n_endpoints (WpSession * self);
diff --git a/lib/wp/spa-props.c b/lib/wp/spa-props.c
index 772d7a18..108779a0 100644
--- a/lib/wp/spa-props.c
+++ b/lib/wp/spa-props.c
@@ -16,12 +16,14 @@
 #include <spa/utils/defs.h>
 #include <spa/utils/result.h>
 
+#include "spa-pod.h"
+#include "spa-type.h"
+
 struct entry
 {
-  guint32 id;
-  gchar *name;
-  struct spa_pod *type;
-  struct spa_pod *value;
+  gchar *id_name;
+  gchar *description;
+  WpSpaPod *value;
 };
 
 struct entry *
@@ -34,9 +36,9 @@ entry_new (void)
 static void
 entry_free (struct entry *e)
 {
-  g_free (e->name);
-  free (e->type);
-  free (e->value);
+  g_free (e->id_name);
+  g_free (e->description);
+  g_clear_pointer (&e->value, wp_spa_pod_unref);
   g_slice_free (struct entry, e);
 }
 
@@ -47,217 +49,163 @@ wp_spa_props_clear (WpSpaProps * self)
   self->entries = NULL;
 }
 
+// Takes ownership of pod
 void
-wp_spa_props_register_pod (WpSpaProps * self,
-    guint32 id, const gchar *name, const struct spa_pod *type)
+wp_spa_props_register (WpSpaProps * self, const char *id_name,
+    const gchar *description, WpSpaPod *pod)
 {
   struct entry *e = entry_new ();
-  e->id = id;
-  e->name = g_strdup (name);
-  e->type = spa_pod_copy (type);
-
-  if (!spa_pod_is_choice (type))
-    e->value = spa_pod_copy (type);
-  else
-    e->value = spa_pod_copy (SPA_POD_CHOICE_CHILD (type));
-
+  e->id_name = g_strdup (id_name);
+  e->description = g_strdup (description);
+  e->value = pod;
   self->entries = g_list_append (self->entries, e);
 }
 
-gint
+gboolean
 wp_spa_props_register_from_prop_info (WpSpaProps * self,
-    const struct spa_pod * prop_info)
+    const WpSpaPod * prop_info)
 {
   guint32 id;
-  gchar *name;
-  const struct spa_pod *type;
-  int res;
-
-  res = spa_pod_parse_object (prop_info,
-      SPA_TYPE_OBJECT_PropInfo, NULL,
-      SPA_PROP_INFO_id,   SPA_POD_Id (&id),
-      SPA_PROP_INFO_name, SPA_POD_String (&name),
-      SPA_PROP_INFO_type, SPA_POD_Pod (&type));
-
-  if (res < 0) {
-    g_debug ("Bad prop info object");
-    return res;
+  const gchar *id_name, *description;
+  g_autoptr (WpSpaPod) type = NULL;
+
+
+  if (!wp_spa_pod_get_object (prop_info,
+      "PropInfo", NULL,
+      "id", "I", &id,
+      "name", "s", &description,
+      "type", "P", &type,
+      NULL)) {
+    g_assert_true (FALSE);
+    g_warning ("Bad prop info object");
+    return FALSE;
+  }
+
+  if (!wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PROPS, id, NULL, &id_name,
+      NULL)) {
+    g_warning ("Id '%d' is not registered", id);
+    return FALSE;
   }
 
-  wp_spa_props_register_pod (self, id, name, type);
-  return 0;
+  wp_spa_props_register (self, id_name, description, g_steal_pointer (&type));
+  return TRUE;
 }
 
 // get <-- cached
-const struct spa_pod *
-wp_spa_props_get_stored (WpSpaProps * self, guint32 id)
+WpSpaPod *
+wp_spa_props_get_stored (WpSpaProps * self, const char * id_name)
 {
   GList *l = self->entries;
-  while (l && ((struct entry *) l->data)->id != id)
+  struct entry * e;
+  while (l && g_strcmp0 (((struct entry *) l->data)->id_name, id_name) != 0)
     l = g_list_next (l);
+  if (!l)
+    return NULL;
 
-  return l ? ((struct entry *) l->data)->value : NULL;
+  e = (struct entry *) l->data;
+  return wp_spa_pod_is_choice (e->value) ?
+      wp_spa_pod_get_choice_child (e->value) : wp_spa_pod_ref (e->value);
 }
 
 // exported set --> cache + update(variant to pod -> push)
-gint
-wp_spa_props_store_pod (WpSpaProps * self, guint32 id,
-    const struct spa_pod * value)
+gboolean
+wp_spa_props_store (WpSpaProps * self, const char * id_name,
+    const WpSpaPod *value)
 {
   GList *l = self->entries;
   struct entry * e;
-  uint32_t expected_type;
+  g_autoptr (WpSpaPod) pod = NULL;
 
-  while (l && ((struct entry *) l->data)->id != id)
+  while (l && g_strcmp0 (((struct entry *) l->data)->id_name, id_name) != 0)
     l = g_list_next (l);
-
   if (!l)
-    return -ESRCH;
+    return FALSE;
 
   e = (struct entry *) l->data;
 
-  expected_type = spa_pod_is_choice (e->type) ?
-      SPA_POD_CHOICE_VALUE_TYPE (e->type) : SPA_POD_TYPE (e->type);
-  if (SPA_POD_TYPE (value) != expected_type)
-    return -EINVAL;
-
-#define GET_VAL(pod, type) ((struct spa_pod_##type *) pod)->value
-
-  switch (SPA_POD_TYPE (value)) {
-    //TODO bounds checking on integer types
-  case SPA_TYPE_Id:
-    if (GET_VAL (e->value, id) != GET_VAL (value, id)) {
-      GET_VAL (e->value, id) = GET_VAL (value, id);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Bool:
-    if (GET_VAL (e->value, bool) != GET_VAL (value, bool)) {
-      GET_VAL (e->value, bool) = GET_VAL (value, bool);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Int:
-    if (GET_VAL (e->value, int) != GET_VAL (value, int)) {
-      GET_VAL (e->value, int) = GET_VAL (value, int);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Long:
-    if (GET_VAL (e->value, long) != GET_VAL (value, long)) {
-      GET_VAL (e->value, long) = GET_VAL (value, long);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Fd:
-    if (GET_VAL (e->value, fd) != GET_VAL (value, fd)) {
-      GET_VAL (e->value, fd) = GET_VAL (value, fd);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Float:
-    if (GET_VAL (e->value, float) != GET_VAL (value, float)) {
-      GET_VAL (e->value, float) = GET_VAL (value, float);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Double:
-    if (GET_VAL (e->value, double) != GET_VAL (value, double)) {
-      GET_VAL (e->value, double) = GET_VAL (value, double);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Rectangle:
-    if (GET_VAL (e->value, rectangle).width != GET_VAL (value, rectangle).width ||
-        GET_VAL (e->value, rectangle).height != GET_VAL (value, rectangle).height) {
-      GET_VAL (e->value, rectangle) = GET_VAL (value, rectangle);
-      return 1;
-    }
-    break;
-  case SPA_TYPE_Fraction:
-    if (GET_VAL (e->value, fraction).num != GET_VAL (value, fraction).num ||
-        GET_VAL (e->value, fraction).denom != GET_VAL (value, fraction).denom) {
-      GET_VAL (e->value, fraction) = GET_VAL (value, fraction);
-      return 1;
-    }
-    break;
-  default:
-    g_clear_pointer (&e->value, free);
-    e->value = spa_pod_copy (value);
-    return 1;
-  }
-
-#undef GET_VAL
+  pod = wp_spa_pod_is_choice (e->value) ?
+      wp_spa_pod_get_choice_child (e->value) : wp_spa_pod_ref (e->value);
 
-  return 0;
+  return !wp_spa_pod_equal (pod, value) && wp_spa_pod_set_pod (pod, value);
 }
 
 // exported event set --> pod to variant -> cache
 // proxy event param --> pod to variant -> cache
-gint
-wp_spa_props_store_from_props (WpSpaProps * self, const struct spa_pod * props,
-    GArray * changed_ids)
+gboolean
+wp_spa_props_store_from_props (WpSpaProps * self, const WpSpaPod * props,
+    GPtrArray * changed_ids)
 {
-  const struct spa_pod_object *obj;
-  const struct spa_pod_prop *iter;
-  gint ret, count = 0;
-
-  g_return_val_if_fail (!changed_ids ||
-      g_array_get_element_size (changed_ids) == sizeof (uint32_t), -EINVAL);
-
-  if (!spa_pod_is_object_type (props, SPA_TYPE_OBJECT_Props))
-    return -EINVAL;
-
-  obj = (const struct spa_pod_object *) props;
-  iter = spa_pod_prop_first (&obj->body);
-
-  for (iter = spa_pod_prop_first (&obj->body);
-       spa_pod_prop_is_inside (&obj->body, obj->pod.size, iter);
-       iter = spa_pod_prop_next (iter)) {
-    if ((ret = wp_spa_props_store_pod (self, iter->key, &iter->value)) < 0) {
-      g_debug ("error storing property 0x%x: %s", iter->key,
-          spa_strerror (ret));
-    } else if (ret == 1 && changed_ids) {
-      g_array_append_val (changed_ids, iter->key);
-      count++;
-    }
+  g_autoptr (WpSpaPod) pod = NULL;
+  g_autoptr (WpIterator) it = NULL;
+  GValue next = G_VALUE_INIT;
+
+  g_return_val_if_fail (self, FALSE);
+  g_return_val_if_fail (props, FALSE);
+  g_return_val_if_fail (changed_ids, FALSE);
+
+  if (g_strcmp0 (wp_spa_pod_get_object_type_name (props), "Props") != 0)
+    return FALSE;
+
+  it = wp_spa_pod_iterator_new (props);
+  while (wp_iterator_next (it, &next)) {
+    WpSpaPod *p = g_value_get_boxed (&next);
+    const char *key_name = NULL;
+    g_autoptr (WpSpaPod) v = NULL;
+    wp_spa_pod_get_property (p, &key_name, &v);
+
+    if (wp_spa_props_store (self, key_name, v) && changed_ids)
+      g_ptr_array_add (changed_ids, g_strdup (key_name));
+
+    g_value_unset (&next);
   }
 
-  return count;
+  return TRUE;
 }
 
-struct spa_pod *
-wp_spa_props_build_props (WpSpaProps * self, struct spa_pod_builder * b)
+WpSpaPod *
+wp_spa_props_build_props (WpSpaProps * self)
 {
-  struct spa_pod_frame f;
+  g_autoptr (WpSpaPodBuilder) b = NULL;
   GList *l;
 
-  spa_pod_builder_push_object (b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
+  g_return_val_if_fail (self, NULL);
+
+  b = wp_spa_pod_builder_new_object ("Props", "Props");
   for (l = self->entries; l != NULL; l = g_list_next (l)) {
     struct entry * e = (struct entry *) l->data;
-    if (e->value) {
-      spa_pod_builder_prop (b, e->id, 0);
-      spa_pod_builder_primitive (b, e->value);
+    if (e->id_name && e->value) {
+      g_autoptr (WpSpaPod) pod = wp_spa_pod_is_choice (e->value) ?
+          wp_spa_pod_get_choice_child (e->value) : wp_spa_pod_ref (e->value);
+      wp_spa_pod_builder_add_property (b, e->id_name);
+      wp_spa_pod_builder_add_pod (b, pod);
     }
   }
-  return spa_pod_builder_pop (b, &f);
+
+  return wp_spa_pod_builder_end (b);
 }
 
 GPtrArray *
-wp_spa_props_build_propinfo (WpSpaProps * self, struct spa_pod_builder * b)
+wp_spa_props_build_propinfo (WpSpaProps * self)
 {
-  GPtrArray *res = g_ptr_array_new ();
+  GPtrArray *res = g_ptr_array_new_with_free_func (
+      (GDestroyNotify) wp_spa_pod_unref);
   GList *l;
-  struct spa_pod *pod;
 
   for (l = self->entries; l != NULL; l = g_list_next (l)) {
     struct entry * e = (struct entry *) l->data;
-    pod = spa_pod_builder_add_object (b,
-        SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
-        SPA_PROP_INFO_id,   SPA_POD_Id (e->id),
-        SPA_PROP_INFO_name, SPA_POD_String (e->name),
-        SPA_PROP_INFO_type, SPA_POD_Pod (e->type));
-    g_ptr_array_add (res, pod);
+    guint32 id;
+    if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PROPS, e->id_name, &id,
+        NULL, NULL)) {
+      g_warning ("Id name '%s' is not registered", e->id_name);
+      continue;
+    }
+
+    g_ptr_array_add (res, wp_spa_pod_new_object (
+        "PropInfo", "PropInfo",
+        "id", "I", id,
+        "name", "s", e->description,
+        "type", "P", e->value,
+        NULL));
   }
 
   return res;
@@ -265,40 +213,9 @@ wp_spa_props_build_propinfo (WpSpaProps * self, struct spa_pod_builder * b)
 
 // for exported update / prop_info + props
 GPtrArray *
-wp_spa_props_build_all_pods (WpSpaProps * self, struct spa_pod_builder * b)
+wp_spa_props_build_all_pods (WpSpaProps * self)
 {
-  GPtrArray *res;
-  struct spa_pod *pod;
-
-  pod = wp_spa_props_build_props (self, b);
-  res = wp_spa_props_build_propinfo (self, b);
-  g_ptr_array_insert (res, 0, pod);
-
+  GPtrArray *res = wp_spa_props_build_propinfo (self);
+  g_ptr_array_insert (res, 0, wp_spa_props_build_props (self));
   return res;
 }
-
-// proxy set --> value to props object -> push
-struct spa_pod *
-wp_spa_props_build_update (WpSpaProps * self, guint32 id,
-    const struct spa_pod * value, struct spa_pod_builder * b)
-{
-  struct spa_pod_frame f;
-
-  spa_pod_builder_push_object (b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props);
-  spa_pod_builder_prop (b, id, 0);
-  spa_pod_builder_primitive (b, value);
-  return spa_pod_builder_pop (b, &f);
-}
-
-const struct spa_pod *
-wp_spa_props_build_pod_valist (gchar * buffer, gsize size, va_list args)
-{
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buffer, size);
-  struct spa_pod_frame f;
-  void *pod;
-
-  spa_pod_builder_push_struct (&b, &f);
-  spa_pod_builder_addv (&b, args);
-  pod = spa_pod_builder_pop (&b, &f);
-  return SPA_POD_CONTENTS (struct spa_pod_struct, pod);
-}
diff --git a/modules/module-config-policy/config-policy.c b/modules/module-config-policy/config-policy.c
index 09988859..e3380f6e 100644
--- a/modules/module-config-policy/config-policy.c
+++ b/modules/module-config-policy/config-policy.c
@@ -300,13 +300,13 @@ wp_config_policy_find_endpoint (WpPolicy *policy, GVariant *props,
   /* Otherwise, use the default session endpoint if the session is valid */
   else if (session) {
     /* Get the default type */
-    WpDefaultEndpointType type;
+    const gchar * type_name;
     switch (data->me.endpoint_data.direction) {
       case PW_DIRECTION_INPUT:
-        type = WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE;
+        type_name = "wp-session-default-endpoint-audio-source";
         break;
       case PW_DIRECTION_OUTPUT:
-        type = WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK;
+        type_name = "wp-session-default-endpoint-audio-sink";
         break;
       default:
         g_warn_if_reached ();
@@ -321,7 +321,7 @@ wp_config_policy_find_endpoint (WpPolicy *policy, GVariant *props,
     /* Find the default session endpoint */
     for (i = 0; i < endpoints->len; i++) {
       target = g_ptr_array_index (endpoints, i);
-      guint def_id = wp_session_get_default_endpoint (session, type);
+      guint def_id = wp_session_get_default_endpoint (session, type_name);
       if (def_id == wp_base_endpoint_get_global_id (target))
         break;
     }
diff --git a/modules/module-monitor.c b/modules/module-monitor.c
index 91715be0..c02059ca 100644
--- a/modules/module-monitor.c
+++ b/modules/module-monitor.c
@@ -362,14 +362,11 @@ device_created (GObject * proxy, GAsyncResult * res, gpointer user_data)
 
   if (data->flags & FLAG_ACTIVATE_DEVICES &&
       !(data->flags & FLAG_DBUS_RESERVATION)) {
-    char buf[1024];
-    struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
-
-    wp_proxy_set_param (WP_PROXY (proxy),
-        SPA_PARAM_Profile, 0,
-        spa_pod_builder_add_object (&b,
-            SPA_TYPE_OBJECT_ParamProfile, 0,
-            SPA_PARAM_PROFILE_index, SPA_POD_Int (1)));
+    g_autoptr (WpSpaPod) profile = wp_spa_pod_new_object (
+      "Profile", "Profile",
+      "index", "i", 1,
+      NULL);
+    wp_proxy_set_param (WP_PROXY (proxy), SPA_PARAM_Profile, 0, profile);
   }
 }
 
diff --git a/modules/module-monitor/reservation-data.c b/modules/module-monitor/reservation-data.c
index 26883909..57b5b642 100644
--- a/modules/module-monitor/reservation-data.c
+++ b/modules/module-monitor/reservation-data.c
@@ -47,8 +47,7 @@ on_reservation_acquired (GObject *obj, GAsyncResult *res, gpointer user_data)
       WP_MONITOR_DBUS_DEVICE_RESERVATION (obj);
   g_autoptr (GError) error = NULL;
   g_autoptr (WpProxy) device = NULL;
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof(buf));
+  g_autoptr (WpSpaPod) profile = NULL;
 
   /* Finish */
   if (!wp_monitor_dbus_device_reservation_async_finish (reserv, res, &error)) {
@@ -62,10 +61,11 @@ on_reservation_acquired (GObject *obj, GAsyncResult *res, gpointer user_data)
     return;
 
   /* Set profile 1 */
-  wp_proxy_set_param (device, SPA_PARAM_Profile, 0,
-      spa_pod_builder_add_object(&b,
-          SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
-          SPA_PARAM_PROFILE_index, SPA_POD_Int(1)));
+  profile = wp_spa_pod_new_object (
+      "Profile", "Profile",
+      "index", "i", 1,
+      NULL);
+  wp_proxy_set_param (device, SPA_PARAM_Profile, 0, profile);
 }
 
 static void
@@ -74,8 +74,7 @@ on_reservation_release (WpMonitorDbusDeviceReservation *reservation, int forced,
 {
   g_autoptr (WpProxy) device = NULL;
   g_autoptr (WpCore) core = NULL;
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof(buf));
+  g_autoptr (WpSpaPod) profile = NULL;
 
   /* Get the device and core */
   device = g_weak_ref_get (&self->device);
@@ -86,10 +85,11 @@ on_reservation_release (WpMonitorDbusDeviceReservation *reservation, int forced,
     return;
 
   /* Set profile 0 */
-  wp_proxy_set_param (device, SPA_PARAM_Profile, 0,
-      spa_pod_builder_add_object(&b,
-          SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
-          SPA_PARAM_PROFILE_index, SPA_POD_Int(0)));
+  profile = wp_spa_pod_new_object (
+      "Profile", "Profile",
+      "index", "i", 0,
+      NULL);
+  wp_proxy_set_param (device, SPA_PARAM_Profile, 0, profile);
 
   /* Complete release on done */
   wp_core_sync (core, NULL, (GAsyncReadyCallback)on_device_done, self);
diff --git a/modules/module-pipewire/algorithms.c b/modules/module-pipewire/algorithms.c
index a7a9c93c..1b74353c 100644
--- a/modules/module-pipewire/algorithms.c
+++ b/modules/module-pipewire/algorithms.c
@@ -129,9 +129,11 @@ invalid_argument:
 
 
 static enum spa_audio_format
-select_format (uint32_t *vals, uint32_t n_vals, uint32_t choice)
+select_format (WpSpaPod *value)
 {
-  enum spa_audio_format fmt_order[] = {
+  enum spa_audio_format ret = SPA_AUDIO_FORMAT_UNKNOWN;
+
+  static enum spa_audio_format fmt_order[] = {
     /* float 32 is the best because it needs
        no conversion from our internal pipeline format */
     SPA_AUDIO_FORMAT_F32,
@@ -188,97 +190,149 @@ select_format (uint32_t *vals, uint32_t n_vals, uint32_t choice)
     SPA_AUDIO_FORMAT_F64P,
     SPA_AUDIO_FORMAT_U8P,
   };
-  uint32_t i, j, best = SPA_N_ELEMENTS(fmt_order);
+  guint32 best = SPA_N_ELEMENTS(fmt_order);
+
+  /* Just return the value if it is not a choice value */
+  if (!wp_spa_pod_is_choice (value)) {
+    wp_spa_pod_get_id (value, &ret);
+    return ret;
+  }
 
-  switch (choice) {
-  case SPA_CHOICE_None:
-    g_return_val_if_fail (n_vals != 0, SPA_AUDIO_FORMAT_UNKNOWN);
-    return vals[0];
+  const gchar * choice_type_name = wp_spa_pod_get_choice_type_name (value);
+
+  /* None */
+  if (g_strcmp0 ("None", choice_type_name) == 0) {
+    g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (value);
+    wp_spa_pod_get_id (child, &ret);
+  }
 
-  case SPA_CHOICE_Enum:
-    for (i = 0; i < n_vals; ++i, ++vals) {
-      for (j = 0; j < SPA_N_ELEMENTS(fmt_order); j++) {
-        if (*vals == fmt_order[j] && best > j) {
+  /* Enum */
+  else if (g_strcmp0 ("Enum", choice_type_name) == 0) {
+    g_autoptr (WpIterator) it = wp_spa_pod_iterator_new (value);
+    GValue next = G_VALUE_INIT;
+    while (wp_iterator_next (it, &next)) {
+      enum spa_audio_format *format_id = (enum spa_audio_format *)
+          g_value_get_pointer (&next);
+      for (guint j = 0; j < SPA_N_ELEMENTS(fmt_order); j++) {
+        if (*format_id == fmt_order[j] && best > j) {
           best = j;
           break;
         }
       }
+      g_value_unset (&next);
     }
-    return (best < SPA_N_ELEMENTS(fmt_order)) ?
-        fmt_order[best] : SPA_AUDIO_FORMAT_UNKNOWN;
-
-  default:
-    g_return_val_if_reached (SPA_AUDIO_FORMAT_UNKNOWN);
+    if (best < SPA_N_ELEMENTS(fmt_order))
+      ret = fmt_order[best];
   }
+
+  return ret;
 }
 
-static int32_t
-select_rate (int32_t *vals, uint32_t n_vals, uint32_t choice)
+static gint
+select_rate (WpSpaPod *value)
 {
-  uint32_t i;
-  int32_t result = 0, min, max;
+  gint ret = 0;
 
-  switch (choice) {
-  case SPA_CHOICE_None:
-    g_return_val_if_fail (n_vals != 0, 0);
-    return vals[0];
+  /* Just return the value if it is not a choice value */
+  if (!wp_spa_pod_is_choice (value)) {
+    wp_spa_pod_get_int (value, &ret);
+    return ret;
+  }
 
-  case SPA_CHOICE_Enum:
+  const gchar * choice_type_name = wp_spa_pod_get_choice_type_name (value);
+
+  /* None */
+  if (g_strcmp0 ("None", choice_type_name) == 0) {
+    g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (value);
+    wp_spa_pod_get_int (child, &ret);
+  }
+
+  /* Enum */
+  else if (g_strcmp0 ("Enum", choice_type_name) == 0) {
     /* pick the one closest to 48Khz */
-    g_return_val_if_fail (n_vals != 0, 0);
-    result = vals[0];
-    for (i = 1, ++vals; i < n_vals; ++i, ++vals) {
-      if (abs(*vals - 48000) < abs(result - 48000))
-        result = *vals;
+    g_autoptr (WpIterator) it = wp_spa_pod_iterator_new (value);
+    GValue next = G_VALUE_INIT;
+    while (wp_iterator_next (it, &next)) {
+      gint *rate = (gint *) g_value_get_pointer (&next);
+      if (abs (*rate - 48000) < abs (ret - 48000))
+        ret = *rate;
+      g_value_unset (&next);
     }
-    return result;
+  }
 
-  case SPA_CHOICE_Range:
+  /* Range */
+  else if (g_strcmp0 ("Range", choice_type_name) == 0) {
     /* a range is typically 3 items: default, min, max;
        however, sometimes ALSA drivers give bad min & max values
        and pipewire picks a bad default... try to fix that here;
        the default should be the one closest to 48K */
-    g_return_val_if_fail (n_vals == 3, 0);
+    g_autoptr (WpIterator) it = wp_spa_pod_iterator_new (value);
+    GValue next = G_VALUE_INIT;
+    gint vals[3];
+    gint i = 0, min, max;
+    while (wp_iterator_next (it, &next) && i < 3) {
+      vals[i] = *(gint *) g_value_get_pointer (&next);
+      g_value_unset (&next);
+      i++;
+    }
     min = SPA_MIN (vals[1], vals[2]);
     max = SPA_MAX (vals[1], vals[2]);
-    return SPA_CLAMP(48000, min, max);
-
-  default:
-    g_return_val_if_reached (0);
+    ret = SPA_CLAMP (48000, min, max);
   }
+
+  return ret;
 }
 
-static uint32_t
-select_channels (uint32_t *vals, uint32_t n_vals, uint32_t choice)
+static gint
+select_channels (WpSpaPod *value)
 {
-  uint32_t i;
-  uint32_t result = 0;
+  gint ret = 0;
 
-  switch (choice) {
-  case SPA_CHOICE_None:
-    g_return_val_if_fail (n_vals != 0, 0);
-    return vals[0];
+  /* Just return the value if it is not a choice value */
+  if (!wp_spa_pod_is_choice (value)) {
+    wp_spa_pod_get_int (value, &ret);
+    return ret;
+  }
+
+  const gchar * choice_type_name = wp_spa_pod_get_choice_type_name (value);
+
+  /* None */
+  if (g_strcmp0 ("None", choice_type_name) == 0) {
+    g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (value);
+    wp_spa_pod_get_int (child, &ret);
+  }
 
-  case SPA_CHOICE_Enum:
+  /* Enum */
+  else if (g_strcmp0 ("Enum", choice_type_name) == 0) {
     /* choose the most channels */
-    g_return_val_if_fail (n_vals != 0, 0);
-    result = vals[0];
-    for (i = 1, ++vals; i < n_vals; ++i, ++vals) {
-      if (*vals > result)
-        result = *vals;
+    g_autoptr (WpIterator) it = wp_spa_pod_iterator_new (value);
+    GValue next = G_VALUE_INIT;
+    while (wp_iterator_next (it, &next)) {
+      gint *channel = (gint *) g_value_get_pointer (&next);
+      if (*channel > ret)
+        ret = *channel;
+      g_value_unset (&next);
     }
-    return result;
+  }
 
-  case SPA_CHOICE_Range:
+  /* Range */
+  else if (g_strcmp0 ("Range", choice_type_name) == 0) {
     /* a range is typically 3 items: default, min, max;
        we want the most channels, but let's not trust max
        to really be the max... ALSA drivers can be broken */
-    g_return_val_if_fail (n_vals == 3, 0);
-    return SPA_MAX (vals[1], vals[2]);
-
-  default:
-    g_return_val_if_reached (0);
+    g_autoptr (WpIterator) it = wp_spa_pod_iterator_new (value);
+    GValue next = G_VALUE_INIT;
+    gint vals[3];
+    gint i = 0;
+    while (wp_iterator_next (it, &next) && i < 3) {
+      vals[i] = *(gint *) g_value_get_pointer (&next);
+      g_value_unset (&next);
+      i++;
+    }
+    ret = SPA_MAX (vals[1], vals[2]);
   }
+
+  return ret;
 }
 
 gboolean
@@ -291,8 +345,7 @@ choose_sensible_raw_audio_format (GPtrArray *formats,
   raw = g_alloca (sizeof (struct spa_audio_info_raw) * formats->len);
 
   for (i = 0; i < formats->len; i++) {
-    struct spa_pod *pod;
-    struct spa_pod_prop *prop;
+    WpSpaPod *pod = g_ptr_array_index (formats, i);
     uint32_t mtype, mstype;
 
     /* initialize all fields to zero (SPA_AUDIO_FORMAT_UNKNOWN etc) and set
@@ -300,14 +353,16 @@ choose_sensible_raw_audio_format (GPtrArray *formats,
     spa_memzero (&raw[i], sizeof(struct spa_audio_info_raw));
     SPA_FLAG_SET(raw[i].flags, SPA_AUDIO_FLAG_UNPOSITIONED);
 
-    pod = g_ptr_array_index (formats, i);
-
-    if (!spa_pod_is_object (pod)) {
+    if (!wp_spa_pod_is_object (pod)) {
       g_warning ("non-object POD appeared on formats list; this node is buggy");
       continue;
     }
 
-    if (spa_format_parse (pod, &mtype, &mstype) < 0) {
+    if (!wp_spa_pod_get_object (pod,
+        "Format", NULL,
+        "mediaType", "I", &mtype,
+        "mediaSubtype", "I", &mstype,
+        NULL)) {
       g_warning ("format does not have media type / subtype");
       continue;
     }
@@ -316,60 +371,48 @@ choose_sensible_raw_audio_format (GPtrArray *formats,
       continue;
 
     /* go through the fields and populate raw[i] */
-    SPA_POD_OBJECT_FOREACH ((struct spa_pod_object *) pod, prop) {
-      uint32_t type, size, n_vals, choice;
-      const struct spa_pod *val;
-      void *vals;
-
-      if (prop->key == SPA_FORMAT_mediaType ||
-          prop->key == SPA_FORMAT_mediaSubtype)
-        continue;
-
-      val = spa_pod_get_values (&prop->value, &n_vals, &choice);
-      type = val->type;
-      size = val->size;
-      vals = SPA_POD_BODY(val);
-
-#define test_invariant(x) \
-  G_STMT_START { \
-    if (G_LIKELY (x)) ; \
-    else { \
-      g_warn_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, #x); \
-      raw[i].format = SPA_AUDIO_FORMAT_UNKNOWN; \
-      goto next; \
-    } \
-  } G_STMT_END
-
-      switch (prop->key) {
-        case SPA_FORMAT_AUDIO_format:
-          test_invariant (type == SPA_TYPE_Id);
-          test_invariant (size == sizeof(uint32_t));
-          raw[i].format = select_format ((uint32_t *) vals, n_vals, choice);
-          break;
-        case SPA_FORMAT_AUDIO_rate:
-          test_invariant (type == SPA_TYPE_Int);
-          test_invariant (size == sizeof(int32_t));
-          raw[i].rate = select_rate ((int32_t *) vals, n_vals, choice);
-          break;
-        case SPA_FORMAT_AUDIO_channels:
-          test_invariant (type == SPA_TYPE_Int);
-          test_invariant (size == sizeof(uint32_t));
-          raw[i].channels = select_channels ((uint32_t *) vals, n_vals, choice);
-          break;
-        case SPA_FORMAT_AUDIO_position:
-          /* just copy the array, there is no choice here */
-          SPA_FLAG_CLEAR (raw[i].flags, SPA_AUDIO_FLAG_UNPOSITIONED);
-          spa_pod_copy_array (val, SPA_TYPE_Id, raw[i].position,
-              SPA_AUDIO_MAX_CHANNELS);
-          break;
-        default:
-          if (prop->value.type == SPA_TYPE_Choice)
-            SPA_POD_CHOICE_TYPE(&prop->value) = SPA_CHOICE_None;
-          break;
+    g_autoptr (WpIterator) it = wp_spa_pod_iterator_new (pod);
+    GValue next = G_VALUE_INIT;
+    while (wp_iterator_next (it, &next)) {
+      WpSpaPod *p = g_value_get_boxed (&next);
+      const gchar *key = NULL;
+      g_autoptr (WpSpaPod) value = NULL;
+      wp_spa_pod_get_property (p, &key, &value);
+
+      /* format */
+      if (g_strcmp0 (key, "format") == 0) {
+        raw[i].format = select_format (value);
+      }
+
+      /* rate */
+      else if (g_strcmp0 (key, "rate") == 0) {
+        raw[i].rate = select_rate (value);
+      }
+
+      /* channels */
+      else if (g_strcmp0 (key, "channels") == 0) {
+        raw[i].channels = select_channels (value);
       }
+
+      /* position */
+      else if (g_strcmp0 (key, "position") == 0) {
+        /* just copy the array, there is no choice here */
+        g_return_val_if_fail (wp_spa_pod_is_array (value), FALSE);
+        SPA_FLAG_CLEAR (raw[i].flags, SPA_AUDIO_FLAG_UNPOSITIONED);
+        g_autoptr (WpIterator) array_it = wp_spa_pod_iterator_new (value);
+        GValue array_next = G_VALUE_INIT;
+        guint j = 0;
+        while (wp_iterator_next (array_it, &array_next)) {
+          guint32 *pos_id = (guint32 *)g_value_get_pointer (&array_next);
+          raw[i].position[j] = *pos_id;
+          g_value_unset (&array_next);
+          j++;
+        }
+      }
+
+      g_value_unset (&next);
     }
 
-next:
     /* figure out if this one is the best so far */
     if (raw[i].format != SPA_AUDIO_FORMAT_UNKNOWN &&
         raw[i].channels > most_channels ) {
diff --git a/modules/module-pipewire/audio-softdsp-endpoint.c b/modules/module-pipewire/audio-softdsp-endpoint.c
index ae4c748f..576d67b7 100644
--- a/modules/module-pipewire/audio-softdsp-endpoint.c
+++ b/modules/module-pipewire/audio-softdsp-endpoint.c
@@ -142,29 +142,26 @@ endpoint_begin_fade (WpBaseEndpoint * ep, guint32 stream_id, guint duration,
 
 #if 0
 static void
-on_exported_control_changed (WpEndpoint * ep, guint32 control_id,
+on_exported_control_changed (WpEndpoint * ep, const gchar *id_name,
     WpPwAudioSoftdspEndpoint *self)
 {
-  switch (control_id) {
-  case WP_ENDPOINT_CONTROL_VOLUME: {
+  g_autoptr (WpSpaPod) ctrl = NULL;
+
+  if (g_strcmp0 (id_name, "volume") == 0) {
     gfloat vol;
-    wp_endpoint_get_control_float (ep, control_id, &vol);
+    ctrl = wp_endpoint_get_control (ep, id_name);
+    wp_spa_pod_get_float (ctrl, &vol);
     wp_audio_stream_set_volume (self->adapter, vol);
-    break;
-  }
-  case WP_ENDPOINT_CONTROL_MUTE: {
+  } else if (g_strcmp0 (id_name, "mute") == 0) {
     gboolean m;
-    wp_endpoint_get_control_boolean (ep, control_id, &m);
+    ctrl = wp_endpoint_get_control (ep, id_name);
+    wp_spa_pod_get_boolean (ctrl, &m);
     wp_audio_stream_set_mute (self->adapter, m);
-    break;
-  }
-  default:
-    break;
   }
 }
 
 static void
-on_adapter_control_changed (WpAudioStream * s, guint32 control_id,
+on_adapter_control_changed (WpAudioStream * s, const gchar *id_name,
     WpPwAudioSoftdspEndpoint *self)
 {
   /* block to avoid recursion - WpEndpoint emits the "control-changed"
@@ -172,21 +169,14 @@ on_adapter_control_changed (WpAudioStream * s, guint32 control_id,
   g_signal_handlers_block_by_func (self->impl_ep,
       on_exported_control_changed, self);
 
-  switch (control_id) {
-  case WP_ENDPOINT_CONTROL_VOLUME: {
+  if (g_strcmp0 (id_name, "volume") == 0) {
     gfloat vol = wp_audio_stream_get_volume (s);
-    wp_endpoint_set_control_float (WP_ENDPOINT (self->impl_ep),
-        control_id, vol);
-    break;
-  }
-  case WP_ENDPOINT_CONTROL_MUTE: {
+    g_autoptr (WpSpaPod) vol_ctrl = wp_spa_pod_new_float (vol);
+    wp_endpoint_set_control (WP_ENDPOINT (self->impl_ep), id_name, vol_ctrl);
+  } else if (g_strcmp0 (id_name, "mute") == 0) {
     gboolean m = wp_audio_stream_get_mute (s);
-    wp_endpoint_set_control_boolean (WP_ENDPOINT (self->impl_ep),
-        control_id, m);
-    break;
-  }
-  default:
-    break;
+    g_autoptr (WpSpaPod) m_ctrl = wp_spa_pod_new_boolean (m);
+    wp_endpoint_set_control (WP_ENDPOINT (self->impl_ep), id_name, m_ctrl);
   }
 
   g_signal_handlers_unblock_by_func (self->impl_ep,
@@ -220,17 +210,15 @@ do_export (WpPwAudioSoftdspEndpoint *self)
   g_autoptr (WpCore) core = wp_base_endpoint_get_core (WP_BASE_ENDPOINT (self));
   g_autoptr (WpProperties) props = NULL;
   g_autoptr (WpProperties) extra_props = NULL;
+  g_autoptr (WpSpaPod) ctrl = NULL;
 
   g_return_if_fail (!self->impl_ep);
 
   self->impl_ep = wp_impl_endpoint_new (core);
 
-  wp_impl_endpoint_register_control (self->impl_ep,
-      WP_ENDPOINT_CONTROL_VOLUME);
-  wp_impl_endpoint_register_control (self->impl_ep,
-      WP_ENDPOINT_CONTROL_MUTE);
-  // wp_impl_endpoint_register_control (self->impl_ep,
-  //     WP_ENDPOINT_CONTROL_CHANNEL_VOLUMES);
+  wp_impl_endpoint_register_control (self->impl_ep, "volume");
+  wp_impl_endpoint_register_control (self->impl_ep, "mute");
+  // wp_impl_endpoint_register_control (self->impl_ep, "channelVolumes");
 
   props = wp_proxy_get_properties (WP_PROXY (self->node));
 
@@ -252,10 +240,10 @@ do_export (WpPwAudioSoftdspEndpoint *self)
   wp_impl_endpoint_set_direction (self->impl_ep,
       wp_base_endpoint_get_direction (WP_BASE_ENDPOINT (self)));
 
-  wp_endpoint_set_control_float (WP_ENDPOINT (self->impl_ep),
-      WP_ENDPOINT_CONTROL_VOLUME, wp_audio_stream_get_volume (self->adapter));
-  wp_endpoint_set_control_boolean (WP_ENDPOINT (self->impl_ep),
-      WP_ENDPOINT_CONTROL_MUTE, wp_audio_stream_get_mute (self->adapter));
+  ctrl = wp_spa_pod_new_float (wp_audio_stream_get_volume (self->adapter));
+  wp_endpoint_set_control (WP_ENDPOINT (self->impl_ep), "volume", ctrl);
+  ctrl = wp_spa_pod_new_boolean (wp_audio_stream_get_mute (self->adapter));
+  wp_endpoint_set_control (WP_ENDPOINT (self->impl_ep), "mute", ctrl);
 
   g_signal_connect_object (self->impl_ep, "control-changed",
       (GCallback) on_exported_control_changed, self, 0);
diff --git a/modules/module-pipewire/audio-softdsp-endpoint/adapter.c b/modules/module-pipewire/audio-softdsp-endpoint/adapter.c
index bb8a3d09..206301e3 100644
--- a/modules/module-pipewire/audio-softdsp-endpoint/adapter.c
+++ b/modules/module-pipewire/audio-softdsp-endpoint/adapter.c
@@ -38,6 +38,34 @@ G_DEFINE_TYPE_WITH_CODE (WpAudioAdapter, wp_audio_adapter, WP_TYPE_AUDIO_STREAM,
     G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
                            wp_audio_adapter_async_initable_init))
 
+static WpSpaPod *
+format_audio_raw_build (const struct spa_audio_info_raw *info)
+{
+  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_object (
+      "Format", "Format");
+  wp_spa_pod_builder_add (builder,
+      "mediaType",    "I", SPA_MEDIA_TYPE_audio,
+      "mediaSubtype", "I", SPA_MEDIA_SUBTYPE_raw,
+      "format",       "I", info->format,
+      "rate",         "i", info->rate,
+      "channels",     "i", info->channels,
+      NULL);
+
+   if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
+     /* Build the position array spa pod */
+     g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
+     for (guint i = 0; i < info->channels; i++)
+       wp_spa_pod_builder_add_id (position_builder, info->position[i]);
+
+     /* Add the position property */
+     wp_spa_pod_builder_add_property (builder, "position");
+     g_autoptr (WpSpaPod) position = wp_spa_pod_builder_end (position_builder);
+     wp_spa_pod_builder_add_pod (builder, position);
+   }
+
+   return wp_spa_pod_builder_end (builder);
+}
+
 static void
 on_proxy_enum_format_done (WpProxy *proxy, GAsyncResult *res,
     WpAudioAdapter *self)
@@ -46,9 +74,9 @@ on_proxy_enum_format_done (WpProxy *proxy, GAsyncResult *res,
   g_autoptr (GError) error = NULL;
   enum pw_direction direction =
       wp_audio_stream_get_direction (WP_AUDIO_STREAM (self));
-  uint8_t buf[1024];
-  struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
   struct spa_pod *param;
+  g_autoptr (WpSpaPod) pod = NULL;
+  g_autoptr (WpSpaPod) format = NULL;
   gboolean control;
 
   formats = wp_proxy_enum_params_collect_finish (proxy, res, &error);
@@ -83,9 +111,8 @@ on_proxy_enum_format_done (WpProxy *proxy, GAsyncResult *res,
   }
 
   /* set the chosen device/client format on the node */
-  param = spa_format_audio_raw_build (&pod_builder, SPA_PARAM_Format,
-      &self->format);
-  wp_proxy_set_param (proxy, SPA_PARAM_Format, 0, param);
+  format = format_audio_raw_build (&self->format);
+  wp_proxy_set_param (proxy, SPA_PARAM_Format, 0, format);
 
   /* now choose the DSP format: keep the chanels but use F32 plannar @ 48K */
   self->format.format = SPA_AUDIO_FORMAT_F32P;
@@ -95,22 +122,23 @@ on_proxy_enum_format_done (WpProxy *proxy, GAsyncResult *res,
   control = direction == PW_DIRECTION_INPUT;
 
   if (self->convert) {
-    param = spa_pod_builder_add_object(&pod_builder,
-        SPA_TYPE_OBJECT_ParamPortConfig,  SPA_PARAM_PortConfig,
-        SPA_PARAM_PORT_CONFIG_direction,  SPA_POD_Id(direction),
-        SPA_PARAM_PORT_CONFIG_mode,       SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_convert),
-        SPA_PARAM_PORT_CONFIG_control,    SPA_POD_Bool(control));
+    pod = wp_spa_pod_new_object ("PortConfig",  "PortConfig",
+        "direction",  "I", pw_direction_reverse(direction),
+        "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_convert,
+        "control",    "b", control,
+        NULL);
   } else {
-    param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &self->format);
-    param = spa_pod_builder_add_object(&pod_builder,
-        SPA_TYPE_OBJECT_ParamPortConfig,  SPA_PARAM_PortConfig,
-        SPA_PARAM_PORT_CONFIG_direction,  SPA_POD_Id(direction),
-        SPA_PARAM_PORT_CONFIG_mode,       SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
-        SPA_PARAM_PORT_CONFIG_control,    SPA_POD_Bool(control),
-        SPA_PARAM_PORT_CONFIG_format,     SPA_POD_Pod(param));
+    /* now choose the DSP format: keep the chanels but use F32 plannar @ 48K */
+    format = format_audio_raw_build (&self->format);
+    pod = wp_spa_pod_new_object ("PortConfig",  "PortConfig",
+        "direction",  "I", direction,
+        "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
+        "control",    "b", control,
+        "format",     "P", format,
+        NULL);
   }
 
-  wp_audio_stream_set_port_config (WP_AUDIO_STREAM (self), param);
+  wp_audio_stream_set_port_config (WP_AUDIO_STREAM (self), pod);
   wp_audio_stream_finish_port_config (WP_AUDIO_STREAM (self));
 }
 
diff --git a/modules/module-pipewire/audio-softdsp-endpoint/convert.c b/modules/module-pipewire/audio-softdsp-endpoint/convert.c
index fc68a6f4..a61304e0 100644
--- a/modules/module-pipewire/audio-softdsp-endpoint/convert.c
+++ b/modules/module-pipewire/audio-softdsp-endpoint/convert.c
@@ -106,6 +106,34 @@ wp_audio_convert_event_info (WpProxy * proxy, GParamSpec *spec,
   }
 }
 
+static WpSpaPod *
+format_audio_raw_build (const struct spa_audio_info_raw *info)
+{
+  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_object (
+      "Format", "Format");
+  wp_spa_pod_builder_add (builder,
+      "mediaType",    "I", SPA_MEDIA_TYPE_audio,
+      "mediaSubtype", "I", SPA_MEDIA_SUBTYPE_raw,
+      "format",       "I", info->format,
+      "rate",         "i", info->rate,
+      "channels",     "i", info->channels,
+      NULL);
+
+   if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
+     /* Build the position array spa pod */
+     g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
+     for (guint i = 0; i < info->channels; i++)
+       wp_spa_pod_builder_add_id (position_builder, info->position[i]);
+
+     /* Add the position property */
+     wp_spa_pod_builder_add_property (builder, "position");
+     g_autoptr (WpSpaPod) position = wp_spa_pod_builder_end (position_builder);
+     wp_spa_pod_builder_add_pod (builder, position);
+   }
+
+   return wp_spa_pod_builder_end (builder);
+}
+
 static void
 on_audio_convert_core_done (WpCore *core, GAsyncResult *res,
     WpAudioConvert *self)
@@ -113,10 +141,8 @@ on_audio_convert_core_done (WpCore *core, GAsyncResult *res,
   g_autoptr (GError) error = NULL;
   enum pw_direction direction =
       wp_audio_stream_get_direction (WP_AUDIO_STREAM (self));
-  uint8_t buf[1024];
-  struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
-  struct spa_pod *format;
-  struct spa_pod *param;
+  g_autoptr (WpSpaPod) format = NULL;
+  g_autoptr (WpSpaPod) pod = NULL;
   gboolean control;
 
   wp_core_sync_finish (core, res, &error);
@@ -129,8 +155,7 @@ on_audio_convert_core_done (WpCore *core, GAsyncResult *res,
 
   g_debug ("%s:%p setting format", G_OBJECT_TYPE_NAME (self), self);
 
-  format = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format,
-      &self->format);
+  format = format_audio_raw_build (&self->format);
 
   /* Only enable control port for input streams */
   control =
@@ -145,20 +170,20 @@ on_audio_convert_core_done (WpCore *core, GAsyncResult *res,
      same format, but with altered volume.
      In the future we need to consider writing a simpler volume node for this,
      as doing merge + split is heavy for our needs */
-  param = spa_pod_builder_add_object(&pod_builder,
-      SPA_TYPE_OBJECT_ParamPortConfig,  SPA_PARAM_PortConfig,
-      SPA_PARAM_PORT_CONFIG_direction,  SPA_POD_Id(pw_direction_reverse(direction)),
-      SPA_PARAM_PORT_CONFIG_mode,       SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
-      SPA_PARAM_PORT_CONFIG_format,     SPA_POD_Pod(format));
-  wp_audio_stream_set_port_config (WP_AUDIO_STREAM (self), param);
-
-  param = spa_pod_builder_add_object(&pod_builder,
-      SPA_TYPE_OBJECT_ParamPortConfig,  SPA_PARAM_PortConfig,
-      SPA_PARAM_PORT_CONFIG_direction,  SPA_POD_Id(direction),
-      SPA_PARAM_PORT_CONFIG_mode,       SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
-      SPA_PARAM_PORT_CONFIG_control,    SPA_POD_Bool(control),
-      SPA_PARAM_PORT_CONFIG_format,     SPA_POD_Pod(format));
-  wp_audio_stream_set_port_config (WP_AUDIO_STREAM (self), param);
+  pod = wp_spa_pod_new_object ("PortConfig",  "PortConfig",
+      "direction",  "I", pw_direction_reverse(direction),
+      "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
+      "format",     "P", format,
+      NULL);
+  wp_audio_stream_set_port_config (WP_AUDIO_STREAM (self), pod);
+
+  pod = wp_spa_pod_new_object ("PortConfig",  "PortConfig",
+      "direction",  "I", direction,
+      "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
+      "control",    "b", control,
+      "format",     "P", format,
+      NULL);
+  wp_audio_stream_set_port_config (WP_AUDIO_STREAM (self), pod);
   wp_audio_stream_finish_port_config (WP_AUDIO_STREAM (self));
 }
 
diff --git a/modules/module-pipewire/audio-softdsp-endpoint/stream.c b/modules/module-pipewire/audio-softdsp-endpoint/stream.c
index 1bb91489..efe01c20 100644
--- a/modules/module-pipewire/audio-softdsp-endpoint/stream.c
+++ b/modules/module-pipewire/audio-softdsp-endpoint/stream.c
@@ -17,10 +17,6 @@
 #include "stream.h"
 
 #if !defined(HAVE_AUDIOFADE)
-# define SPA_PROP_audioFadeDuration 0x30001
-# define SPA_PROP_audioFadeStep 0x30002
-# define SPA_PROP_audioFadeDirection 0x30003
-# define SPA_PROP_audioFadeType 0x30004
 # define SPA_NAME_CONTROL_AUDIO_FADE_SOURCE "control.audio.fade.source"
 #endif
 
@@ -101,14 +97,12 @@ audio_stream_event_param (WpProxy *proxy, int seq, uint32_t id,
         case SPA_PROP_volume:
           spa_pod_get_float(&prop->value, &volume);
           priv->volume = volume;
-          g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0,
-              WP_ENDPOINT_CONTROL_VOLUME);
+          g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, "volume");
           break;
         case SPA_PROP_mute:
           spa_pod_get_bool(&prop->value, &mute);
           priv->mute = mute;
-          g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0,
-              WP_ENDPOINT_CONTROL_MUTE);
+          g_signal_emit (self, signals[SIGNAL_CONTROL_CHANGED], 0, "mute");
           break;
         default:
           break;
@@ -566,7 +560,7 @@ wp_audio_stream_class_init (WpAudioStreamClass * klass)
 
   signals[SIGNAL_CONTROL_CHANGED] = g_signal_new (
       "control-changed", G_TYPE_FROM_CLASS (klass),
-      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
+      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
 }
 
 WpAudioStream *
@@ -747,9 +741,7 @@ wp_audio_stream_begin_fade (WpAudioStream * self, guint duration,
   WpAudioStreamPrivate *priv = wp_audio_stream_get_instance_private (self);
   g_autoptr (WpCore) core = wp_audio_stream_get_core (self);
   GTask *task = NULL;
-  struct spa_pod *props;
-  uint8_t buffer[1024];
-  struct spa_pod_builder b = { 0 };
+  g_autoptr (WpSpaPod) props = NULL;
 
   /* Create the fade callback */
   task = g_task_new (self, cancellable, callback, user_data);
@@ -776,12 +768,13 @@ wp_audio_stream_begin_fade (WpAudioStream * self, guint duration,
   priv->fade_task = task;
 
   /* Send the fade */
-  spa_pod_builder_init(&b, buffer, sizeof(buffer));
-  props = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Props, 0,
-      SPA_PROP_audioFadeDuration,  SPA_POD_Int(duration),
-      SPA_PROP_audioFadeStep,      SPA_POD_Double(step),
-      SPA_PROP_audioFadeDirection, SPA_POD_Id(direction),
-      SPA_PROP_audioFadeType,      SPA_POD_Id(type));
+  props = wp_spa_pod_new_object (
+      "Props", "Props",
+      "audioFadeDuration", "i", duration,
+      "audioFadeStep", "d", step,
+      "audioFadeDirection", "I", direction,
+      "audioFadeType", "I", type,
+      NULL);
   wp_proxy_set_param (WP_PROXY (priv->audio_fade_source), SPA_PARAM_Props, 0,
       props);
 
@@ -824,7 +817,7 @@ wp_audio_stream_init_task_finish (WpAudioStream * self, GError * err)
 
 void
 wp_audio_stream_set_port_config (WpAudioStream * self,
-    const struct spa_pod * param)
+    const WpSpaPod * param)
 {
   WpAudioStreamPrivate *priv = wp_audio_stream_get_instance_private (self);
 
diff --git a/modules/module-pipewire/audio-softdsp-endpoint/stream.h b/modules/module-pipewire/audio-softdsp-endpoint/stream.h
index 3604cc6a..487a87d7 100644
--- a/modules/module-pipewire/audio-softdsp-endpoint/stream.h
+++ b/modules/module-pipewire/audio-softdsp-endpoint/stream.h
@@ -45,7 +45,7 @@ void wp_audio_stream_begin_fade (WpAudioStream * self, guint duration,
 WpCore *wp_audio_stream_get_core (WpAudioStream * self);
 void wp_audio_stream_init_task_finish (WpAudioStream * self, GError * error);
 void wp_audio_stream_set_port_config (WpAudioStream * self,
-    const struct spa_pod * param);
+    const WpSpaPod * param);
 void wp_audio_stream_finish_port_config (WpAudioStream * self);
 
 G_END_DECLS
diff --git a/modules/module-si-adapter.c b/modules/module-si-adapter.c
index 0cd8d5b0..17f8a933 100644
--- a/modules/module-si-adapter.c
+++ b/modules/module-si-adapter.c
@@ -260,6 +260,34 @@ on_ports_changed (WpObjectManager *om, WpTransition * transition)
   wp_transition_advance (transition);
 }
 
+static WpSpaPod *
+format_audio_raw_build (const struct spa_audio_info_raw *info)
+{
+  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_object (
+      "Format", "Format");
+  wp_spa_pod_builder_add (builder,
+      "mediaType",    "I", SPA_MEDIA_TYPE_audio,
+      "mediaSubtype", "I", SPA_MEDIA_SUBTYPE_raw,
+      "format",       "I", info->format,
+      "rate",         "i", info->rate,
+      "channels",     "i", info->channels,
+      NULL);
+
+   if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
+     /* Build the position array spa pod */
+     g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
+     for (guint i = 0; i < info->channels; i++)
+       wp_spa_pod_builder_add_id (position_builder, info->position[i]);
+
+     /* Add the position property */
+     wp_spa_pod_builder_add_property (builder, "position");
+     g_autoptr (WpSpaPod) position = wp_spa_pod_builder_end (position_builder);
+     wp_spa_pod_builder_add_pod (builder, position);
+   }
+
+   return wp_spa_pod_builder_end (builder);
+}
+
 static void
 si_adapter_activate_execute_step (WpSessionItem * item,
     WpTransition * transition, guint step)
@@ -283,30 +311,26 @@ si_adapter_activate_execute_step (WpSessionItem * item,
       break;
 
     case STEP_CONFIGURE_PORTS: {
-      uint8_t buf[1024];
-      struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT (buf, sizeof(buf));
-      struct spa_pod *param;
+      g_autoptr (WpSpaPod) format = NULL;
+      g_autoptr (WpSpaPod) pod = NULL;
 
       /* set the chosen device/client format on the node */
-      param = spa_format_audio_raw_build (&pod_builder, SPA_PARAM_Format,
-          &self->format);
-      wp_proxy_set_param (WP_PROXY (self->node), SPA_PARAM_Format, 0, param);
+      format = format_audio_raw_build (&self->format);
+      wp_proxy_set_param (WP_PROXY (self->node), SPA_PARAM_Format, 0, format);
 
       /* now choose the DSP format: keep the chanels but use F32 plannar @ 48K */
       self->format.format = SPA_AUDIO_FORMAT_F32P;
       self->format.rate = 48000;
 
-      param = spa_format_audio_raw_build (&pod_builder,
-          SPA_PARAM_Format, &self->format);
-      param = spa_pod_builder_add_object (&pod_builder,
-          SPA_TYPE_OBJECT_ParamPortConfig,  SPA_PARAM_PortConfig,
-          SPA_PARAM_PORT_CONFIG_direction,  SPA_POD_Id(self->direction),
-          SPA_PARAM_PORT_CONFIG_mode,       SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp),
-          SPA_PARAM_PORT_CONFIG_monitor,    SPA_POD_Bool(self->monitor),
-          SPA_PARAM_PORT_CONFIG_control,    SPA_POD_Bool(self->control_port),
-          SPA_PARAM_PORT_CONFIG_format,     SPA_POD_Pod(param));
-
-      wp_proxy_set_param (WP_PROXY (self->node), SPA_PARAM_PortConfig, 0, param);
+      format = format_audio_raw_build (&self->format);
+      pod = wp_spa_pod_new_object ("PortConfig",  "PortConfig",
+	  "direction",  "I", self->direction,
+	  "mode",       "I", SPA_PARAM_PORT_CONFIG_MODE_dsp,
+	  "monitor",    "b", self->monitor,
+	  "control",    "b", self->control_port,
+	  "format",     "P", format,
+	  NULL);
+      wp_proxy_set_param (WP_PROXY (self->node), SPA_PARAM_PortConfig, 0, pod);
 
       g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self->node));
       wp_core_sync (core, NULL,
diff --git a/tests/modules/algorithms.c b/tests/modules/algorithms.c
index 0993aa94..9a7908f3 100644
--- a/tests/modules/algorithms.c
+++ b/tests/modules/algorithms.c
@@ -19,76 +19,66 @@
 static void
 test_choose_sensible_raw_audio_format (void)
 {
+  wp_spa_type_init (TRUE);
+
   uint32_t layout[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR,
                         SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_LFE,
                         SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR };
-  guint8 buffer[2048];
-  struct spa_pod_builder b;
-  struct spa_pod_frame f;
-  struct spa_pod *param;
   struct spa_audio_info_raw info;
-  g_autoptr (GPtrArray) formats = g_ptr_array_new ();
+  g_autoptr (GPtrArray) formats =
+      g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
 
   /* test 1 */
   g_ptr_array_remove_range (formats, 0, formats->len);
-  spa_pod_builder_init(&b, buffer, sizeof(buffer));
-  spa_pod_builder_push_object (&b, &f, SPA_TYPE_OBJECT_Format, 0);
-  spa_pod_builder_add (&b,
-      SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-      SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-      SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(3,
-                                    SPA_AUDIO_FORMAT_F32_OE,
-                                    SPA_AUDIO_FORMAT_S16,
-                                    SPA_AUDIO_FORMAT_S20),
-      SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(22000, 44100, 8000),
-      SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 8),
-      0);
-  param = (struct spa_pod *) spa_pod_builder_pop (&b, &f);
-  g_assert_nonnull (param);
-  g_ptr_array_add (formats, param);
-
+  g_autoptr (WpSpaPod) param1 = wp_spa_pod_new_object (
+      "Format", "Format",
+      "mediaType",    SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+      "mediaSubtype", SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+      "format",       SPA_POD_CHOICE_ENUM_Id(3,
+                          SPA_AUDIO_FORMAT_F32_OE,
+                          SPA_AUDIO_FORMAT_S16,
+                          SPA_AUDIO_FORMAT_S20),
+      "rate",         SPA_POD_CHOICE_RANGE_Int(22000, 44100, 8000),
+      "channels",     SPA_POD_CHOICE_RANGE_Int(2, 1, 8),
+      NULL);
+  g_assert_nonnull (param1);
+  g_ptr_array_add (formats, g_steal_pointer (&param1));
   g_assert_true (choose_sensible_raw_audio_format (formats, &info));
   g_assert_cmpint (info.format, ==, SPA_AUDIO_FORMAT_S16);
   g_assert_cmpint (info.rate, ==, 44100);
   g_assert_cmpint (info.channels, ==, 8);
   g_assert_cmpint (info.flags, ==, SPA_AUDIO_FLAG_UNPOSITIONED);
 
-
   /* test 2 */
   g_ptr_array_remove_range (formats, 0, formats->len);
-  spa_pod_builder_init (&b, buffer, sizeof(buffer));
-  spa_pod_builder_push_object (&b, &f, SPA_TYPE_OBJECT_Format, 0);
-  spa_pod_builder_add (&b,
-      SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-      SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-      SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(3,
-                                    SPA_AUDIO_FORMAT_S32,
-                                    SPA_AUDIO_FORMAT_U8,
-                                    SPA_AUDIO_FORMAT_F32),
-      SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(56000, 44100, 96000),
-      SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2),
-      SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, 2, layout),
-      0);
-  param = (struct spa_pod *) spa_pod_builder_pop (&b, &f);
-  g_assert_nonnull (param);
-  g_ptr_array_add (formats, param);
-
-  spa_pod_builder_push_object (&b, &f, SPA_TYPE_OBJECT_Format, 0);
-  spa_pod_builder_add (&b,
-      SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_audio),
-      SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
-      SPA_FORMAT_AUDIO_format,   SPA_POD_CHOICE_ENUM_Id(3,
-                                    SPA_AUDIO_FORMAT_S32,
-                                    SPA_AUDIO_FORMAT_U8,
-                                    SPA_AUDIO_FORMAT_F32),
-      SPA_FORMAT_AUDIO_rate,     SPA_POD_CHOICE_RANGE_Int(56000, 44100, 96000),
-      SPA_FORMAT_AUDIO_channels, SPA_POD_Int(5),
-      SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, 5, layout),
-      0);
-  param = (struct spa_pod *) spa_pod_builder_pop (&b, &f);
-  g_assert_nonnull (param);
-  g_ptr_array_add (formats, param);
-
+  g_autoptr (WpSpaPod) param2 = wp_spa_pod_new_object (
+      "Format", "Format",
+      "mediaType",    SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+      "mediaSubtype", SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+      "format",       SPA_POD_CHOICE_ENUM_Id(3,
+                          SPA_AUDIO_FORMAT_S32,
+                          SPA_AUDIO_FORMAT_U8,
+                          SPA_AUDIO_FORMAT_F32),
+      "rate",         SPA_POD_CHOICE_RANGE_Int(56000, 44100, 96000),
+      "channels",     SPA_POD_Int(2),
+      "position",     SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, 2, layout),
+      NULL);
+  g_assert_nonnull (param2);
+  g_ptr_array_add (formats, g_steal_pointer (&param2));
+  g_autoptr (WpSpaPod) param3 = wp_spa_pod_new_object (
+      "Format", "Format",
+      "mediaType",    SPA_POD_Id(SPA_MEDIA_TYPE_audio),
+      "mediaSubtype", SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
+      "format",       SPA_POD_CHOICE_ENUM_Id(3,
+                          SPA_AUDIO_FORMAT_S32,
+                          SPA_AUDIO_FORMAT_U8,
+                          SPA_AUDIO_FORMAT_F32),
+      "rate",         SPA_POD_CHOICE_RANGE_Int(56000, 44100, 96000),
+      "channels",     SPA_POD_Int(5),
+      "position",     SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, 5, layout),
+      NULL);
+  g_assert_nonnull (param3);
+  g_ptr_array_add (formats, g_steal_pointer (&param3));
   g_assert_true (choose_sensible_raw_audio_format (formats, &info));
   g_assert_cmpint (info.format, ==, SPA_AUDIO_FORMAT_F32);
   g_assert_cmpint (info.rate, ==, 48000);
@@ -100,6 +90,8 @@ test_choose_sensible_raw_audio_format (void)
   g_assert_cmpint (info.position[3], ==, layout[3]);
   g_assert_cmpint (info.position[4], ==, layout[4]);
   g_assert_cmpint (info.position[5], ==, 0);
+
+  wp_spa_type_deinit ();
 }
 
 int
diff --git a/tests/wp/endpoint.c b/tests/wp/endpoint.c
index b9d94437..06e2aabd 100644
--- a/tests/wp/endpoint.c
+++ b/tests/wp/endpoint.c
@@ -159,6 +159,8 @@ test_endpoint_disconnected (WpCore *core, TestEndpointFixture *fixture)
 static void
 test_endpoint_setup (TestEndpointFixture *self, gconstpointer user_data)
 {
+  wp_spa_type_init (TRUE);
+
   g_autoptr (WpProperties) props = NULL;
 
   wp_test_server_setup (&self->server);
@@ -208,6 +210,8 @@ test_endpoint_teardown (TestEndpointFixture *self, gconstpointer user_data)
   g_clear_pointer (&self->loop, g_main_loop_unref);
   g_clear_pointer (&self->context, g_main_context_unref);
   wp_test_server_teardown (&self->server);
+
+  wp_spa_type_deinit ();
 }
 
 static void
@@ -320,10 +324,9 @@ test_endpoint_basic_session_bound (WpProxy * session, GAsyncResult * res,
 #if 0
 static void
 test_endpoint_basic_control_changed (WpEndpoint * endpoint,
-    guint32 control_id, TestEndpointFixture *fixture)
+    const gchar * id_name, TestEndpointFixture *fixture)
 {
-  g_debug ("endpoint changed: %s (0x%x)", G_OBJECT_TYPE_NAME (endpoint),
-      control_id);
+  g_debug ("endpoint changed: %s (%s)", G_OBJECT_TYPE_NAME (endpoint), id_name);
 
   g_assert_true (WP_IS_ENDPOINT (endpoint));
 
@@ -351,6 +354,7 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
   g_autoptr (WpImplSession) session = NULL;
   // gfloat float_value;
   // gboolean boolean_value;
+  // g_autoptr (WpSpaPod) ctrl = NULL;
 
   /* set up the export side */
   g_signal_connect (fixture->export_om, "object-added",
@@ -467,9 +471,9 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
       (GCallback) test_endpoint_basic_notify_properties, fixture);
 
   /* change control on the proxy */
-  g_assert_true (wp_endpoint_set_control_float (
-          WP_ENDPOINT (fixture->proxy_endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, 1.0f));
+  ctrl = wp_spa_pod_new_float (1.0f);
+  g_assert_true (wp_endpoint_set_control (WP_ENDPOINT (fixture->proxy_endpoint),
+      "volume", ctrl));
 
   /* run until the change is on both sides */
   fixture->n_events = 0;
@@ -478,26 +482,28 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
 
   /* test round 2: verify the value change on both sides */
 
-  g_assert_true (wp_endpoint_get_control_float (
-          WP_ENDPOINT (fixture->proxy_endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, &float_value));
-  g_assert_true (wp_endpoint_get_control_boolean (
-          WP_ENDPOINT (fixture->proxy_endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, &boolean_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (fixture->proxy_endpoint), "volume");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_float (ctrl, &float_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (fixture->proxy_endpoint), "mute");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_boolean (ctrl, &boolean_value));
   g_assert_cmpfloat_with_epsilon (float_value, 1.0f, 0.001);
   g_assert_cmpint (boolean_value, ==, TRUE);
 
-  g_assert_true (wp_endpoint_get_control_float (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, &float_value));
-  g_assert_true (wp_endpoint_get_control_boolean (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, &boolean_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (endpoint), "volume");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_float (ctrl, &float_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (endpoint), "mute");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_boolean (ctrl, &boolean_value));
   g_assert_cmpfloat_with_epsilon (float_value, 1.0f, 0.001);
   g_assert_cmpint (boolean_value, ==, TRUE);
 
   /* change control on the impl */
   fixture->n_events = 0;
-  g_assert_true (wp_endpoint_set_control_boolean (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, FALSE));
+  ctrl = wp_spa_pod_new_boolean (FALSE);
+  g_assert_true (wp_endpoint_set_control (WP_ENDPOINT (endpoint), "mute", ctrl));
 
   /* run until the change is on both sides */
   g_main_loop_run (fixture->loop);
@@ -505,19 +511,21 @@ test_endpoint_basic (TestEndpointFixture *fixture, gconstpointer data)
 
   /* test round 3: verify the value change on both sides */
 
-  g_assert_true (wp_endpoint_get_control_float (
-          WP_ENDPOINT (fixture->proxy_endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, &float_value));
-  g_assert_true (wp_endpoint_get_control_boolean (
-          WP_ENDPOINT (fixture->proxy_endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, &boolean_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (fixture->proxy_endpoint), "volume");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_float (ctrl, &float_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (fixture->proxy_endpoint), "mute");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_boolean (ctrl, &boolean_value));
   g_assert_cmpfloat_with_epsilon (float_value, 1.0f, 0.001);
   g_assert_cmpint (boolean_value, ==, FALSE);
 
-  g_assert_true (wp_endpoint_get_control_float (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_VOLUME, &float_value));
-  g_assert_true (wp_endpoint_get_control_boolean (WP_ENDPOINT (endpoint),
-          WP_ENDPOINT_CONTROL_MUTE, &boolean_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (endpoint), "volume");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_float (ctrl, &float_value));
+  ctrl = wp_endpoint_get_control (WP_ENDPOINT (endpoint), "mute");
+  g_assert_nonnull (ctrl);
+  g_assert_true (wp_spa_pod_get_boolean (ctrl, &boolean_value));
   g_assert_cmpfloat_with_epsilon (float_value, 1.0f, 0.001);
   g_assert_cmpint (boolean_value, ==, FALSE);
 
diff --git a/tests/wp/proxy.c b/tests/wp/proxy.c
index f0fa81c5..df311e0d 100644
--- a/tests/wp/proxy.c
+++ b/tests/wp/proxy.c
@@ -173,8 +173,8 @@ test_node_enum_params_done (WpProxy *node, GAsyncResult *res,
   g_assert_cmpint (params->len, ==, data->n_params);
 
   for (i = 0; i < params->len; i++) {
-    struct spa_pod *pod = g_ptr_array_index(params, i);
-    g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_PropInfo));
+    WpSpaPod *pod = g_ptr_array_index (params, i);
+    g_assert_cmpstr ("PropInfo", ==, wp_spa_pod_get_object_type_name (pod));
   }
 
   g_main_loop_quit (data->fixture->loop);
@@ -213,8 +213,9 @@ test_node_object_added (WpObjectManager *om, WpProxy *proxy,
 
   g_signal_connect (proxy, "param", (GCallback) test_node_param,
       param_data);
+  g_autoptr (WpSpaPod) filter = wp_spa_pod_new_none ();
   wp_proxy_enum_params_collect (proxy, SPA_PARAM_PropInfo, 0, -1,
-      NULL, NULL, (GAsyncReadyCallback) test_node_enum_params_done,
+      filter, NULL, (GAsyncReadyCallback) test_node_enum_params_done,
       param_data);
 }
 
diff --git a/tests/wp/session.c b/tests/wp/session.c
index 20da891b..5709c0fc 100644
--- a/tests/wp/session.c
+++ b/tests/wp/session.c
@@ -57,6 +57,9 @@ test_session_disconnected (WpCore *core, TestSessionFixture *fixture)
 static void
 test_session_setup (TestSessionFixture *self, gconstpointer user_data)
 {
+  /* Register custom wireplumber session types */
+  wp_spa_type_init (TRUE);
+
   g_autoptr (WpProperties) props = NULL;
 
   wp_test_server_setup (&self->server);
@@ -106,6 +109,8 @@ test_session_teardown (TestSessionFixture *self, gconstpointer user_data)
   g_clear_pointer (&self->loop, g_main_loop_unref);
   g_clear_pointer (&self->context, g_main_context_unref);
   wp_test_server_teardown (&self->server);
+
+  wp_spa_type_deinit ();
 }
 
 static void
@@ -187,10 +192,10 @@ test_session_basic_export_done (WpProxy * session, GAsyncResult * res,
 
 static void
 test_session_basic_default_endpoint_changed (WpSession * session,
-    WpDefaultEndpointType type, guint32 id, TestSessionFixture *fixture)
+    const char *type_name, guint32 id, TestSessionFixture *fixture)
 {
-  g_debug ("endpoint changed: %s (%u, %u)", G_OBJECT_TYPE_NAME (session),
-      type, id);
+  g_debug ("endpoint changed: %s (%s, %u)", G_OBJECT_TYPE_NAME (session),
+      type_name, id);
 
   g_assert_true (WP_IS_SESSION (session));
 
@@ -243,9 +248,9 @@ test_session_basic (TestSessionFixture *fixture, gconstpointer data)
   session = wp_impl_session_new (fixture->export_core);
   wp_impl_session_set_property (session, "test.property", "test-value");
   wp_session_set_default_endpoint (WP_SESSION (session),
-      WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK, 5);
+      "wp-session-default-endpoint-audio-sink", 5);
   wp_session_set_default_endpoint (WP_SESSION (session),
-      WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE, 9);
+      "wp-session-default-endpoint-video-source", 9);
 
   /* verify properties are set before export */
   {
@@ -255,9 +260,9 @@ test_session_basic (TestSessionFixture *fixture, gconstpointer data)
         "test-value");
   }
   g_assert_cmpuint (wp_session_get_default_endpoint (WP_SESSION (session),
-          WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK), ==, 5);
+          "wp-session-default-endpoint-audio-sink"), ==, 5);
   g_assert_cmpuint (wp_session_get_default_endpoint (WP_SESSION (session),
-          WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE), ==, 9);
+          "wp-session-default-endpoint-video-source"), ==, 9);
 
   /* do export */
   wp_proxy_augment (WP_PROXY (session), WP_PROXY_FEATURE_BOUND, NULL,
@@ -290,10 +295,10 @@ test_session_basic (TestSessionFixture *fixture, gconstpointer data)
   }
   g_assert_cmpuint (wp_session_get_default_endpoint (
           WP_SESSION (fixture->proxy_session),
-          WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK), ==, 5);
+          "wp-session-default-endpoint-audio-sink"), ==, 5);
   g_assert_cmpuint (wp_session_get_default_endpoint (
           WP_SESSION (fixture->proxy_session),
-          WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE), ==, 9);
+          "wp-session-default-endpoint-video-source"), ==, 9);
 
   /* setup change signals */
   g_signal_connect (fixture->proxy_session, "default-endpoint-changed",
@@ -307,7 +312,7 @@ test_session_basic (TestSessionFixture *fixture, gconstpointer data)
 
   /* change default endpoint on the proxy */
   wp_session_set_default_endpoint (WP_SESSION (fixture->proxy_session),
-      WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK, 73);
+      "wp-session-default-endpoint-audio-sink", 73);
 
   /* run until the change is on both sides */
   fixture->n_events = 0;
@@ -318,20 +323,20 @@ test_session_basic (TestSessionFixture *fixture, gconstpointer data)
 
   g_assert_cmpuint (wp_session_get_default_endpoint (
           WP_SESSION (fixture->proxy_session),
-          WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK), ==, 73);
+          "wp-session-default-endpoint-audio-sink"), ==, 73);
   g_assert_cmpuint (wp_session_get_default_endpoint (
           WP_SESSION (fixture->proxy_session),
-          WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE), ==, 9);
+          "wp-session-default-endpoint-video-source"), ==, 9);
 
   g_assert_cmpuint (wp_session_get_default_endpoint (
-          WP_SESSION (session), WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK), ==, 73);
+          WP_SESSION (session), "wp-session-default-endpoint-audio-sink"), ==, 73);
   g_assert_cmpuint (wp_session_get_default_endpoint (
-          WP_SESSION (session), WP_DEFAULT_ENDPOINT_TYPE_VIDEO_SOURCE), ==, 9);
+          WP_SESSION (session), "wp-session-default-endpoint-video-source"), ==, 9);
 
   /* change default endpoint on the exported */
   fixture->n_events = 0;
   wp_session_set_default_endpoint (WP_SESSION (session),
-      WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE, 44);
+      "wp-session-default-endpoint-audio-source", 44);
 
   /* run until the change is on both sides */
   g_main_loop_run (fixture->loop);
@@ -340,10 +345,10 @@ test_session_basic (TestSessionFixture *fixture, gconstpointer data)
   /* test round 3: verify the value change on both sides */
 
   g_assert_cmpuint (wp_session_get_default_endpoint (
-          WP_SESSION (session), WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE), ==, 44);
+          WP_SESSION (session), "wp-session-default-endpoint-audio-source"), ==, 44);
   g_assert_cmpuint (wp_session_get_default_endpoint (
           WP_SESSION (fixture->proxy_session),
-          WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE), ==, 44);
+          "wp-session-default-endpoint-audio-source"), ==, 44);
 
   /* change a property on the exported */
   fixture->n_events = 0;
diff --git a/tests/wp/spa-props.c b/tests/wp/spa-props.c
index 415c37a8..a67cece7 100644
--- a/tests/wp/spa-props.c
+++ b/tests/wp/spa-props.c
@@ -8,240 +8,251 @@
 
 /* private functions, they should be hidden in the shared library */
 #include "wp/spa-props.c"
+#include "wp/spa-type.c"
+#include "wp/iterator.c"
+#include "wp/spa-pod.c"
 
 #include <spa/pod/iter.h>
 
+#include <wp/wp.h>
+
 static void
 test_spa_props_set_get (void)
 {
+  wp_spa_type_init (TRUE);
+  g_assert_true (wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS, "Wp:Test:Property", "wp-test-property"));
+
   WpSpaProps props = {0};
-  const struct spa_pod *pod;
+  g_autoptr (WpSpaPod) pod = NULL;
   float float_value = 0.0;
   const gchar *string_value = NULL;
 
-  wp_spa_props_register (&props, SPA_PROP_volume, "Volume",
-      SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0));
-  wp_spa_props_register (&props, SPA_PROP_START_CUSTOM + 1, "Test property",
-      SPA_POD_String ("default value"));
+  wp_spa_props_register (&props, "volume", "Volume",
+      wp_spa_pod_new_choice ("Range", "f", 1.0, "f", 0.0, "f", 10.0, NULL));
+  wp_spa_props_register (&props, "wp-test-property", "Test property",
+      wp_spa_pod_new_string ("default value"));
 
-  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, SPA_PROP_volume));
-  g_assert_cmpint (spa_pod_get_float (pod, &float_value), ==, 0);
+  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, "volume"));
+  g_assert_true (wp_spa_pod_get_float (pod, &float_value));
   g_assert_cmpfloat_with_epsilon (float_value, 1.0, 0.001);
 
-  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, SPA_PROP_START_CUSTOM + 1));
-  g_assert_cmpint (spa_pod_get_string (pod, &string_value), ==, 0);
+  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, "wp-test-property"));
+  g_assert_true (wp_spa_pod_get_string (pod, &string_value));
   g_assert_cmpstr (string_value, ==, "default value");
 
-  g_assert_cmpint (wp_spa_props_store (&props, SPA_PROP_volume,
-          SPA_POD_Float (0.8)), ==, 1);
-  g_assert_cmpint (wp_spa_props_store (&props, SPA_PROP_START_CUSTOM + 1,
-          SPA_POD_String ("test value")), ==, 1);
+  g_autoptr (WpSpaPod) new_float = wp_spa_pod_new_float (0.8);
+  g_autoptr (WpSpaPod) new_str = wp_spa_pod_new_string ("test value");
+  g_assert_true (wp_spa_props_store (&props, "volume", new_float));
+  g_assert_true (wp_spa_props_store (&props, "wp-test-property", new_str));
 
-  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, SPA_PROP_volume));
-  g_assert_cmpint (spa_pod_get_float (pod, &float_value), ==, 0);
+  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, "volume"));
+  g_assert_true (wp_spa_pod_get_float (pod, &float_value));
   g_assert_cmpfloat_with_epsilon (float_value, 0.8, 0.001);
 
-  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, SPA_PROP_START_CUSTOM + 1));
-  g_assert_cmpint (spa_pod_get_string (pod, &string_value), ==, 0);
+  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, "wp-test-property"));
+  g_assert_true (wp_spa_pod_get_string (pod, &string_value));
   g_assert_cmpstr (string_value, ==, "test value");
 
   wp_spa_props_clear (&props);
+
+  wp_spa_type_deinit ();
 }
 
 static void
 test_spa_props_build_all (void)
 {
+  wp_spa_type_init (TRUE);
+  g_assert_true (wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS, "Wp:Test:Property", "wp-test-property"));
+
   WpSpaProps props = {0};
-  gchar buffer[512];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buffer, sizeof (buffer));
-  struct spa_pod *pod;
+  WpSpaPod *pod = NULL;
   float float_value = 0.0;
   const gchar *string_value = NULL;
-  guint32 id;
+  g_autoptr (WpSpaPod) pod_value = NULL;
   g_autoptr (GPtrArray) arr = NULL;
+  const gchar *id_name;
+  guint32 id;
 
-  wp_spa_props_register (&props, SPA_PROP_volume, "Volume",
-      SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0));
-  wp_spa_props_register (&props, SPA_PROP_START_CUSTOM + 1, "Test property",
-      SPA_POD_String ("default value"));
+  wp_spa_props_register (&props, "volume", "Volume",
+      wp_spa_pod_new_choice ("Range", "f", 1.0, "f", 0.0, "f", 10.0, NULL));
+  wp_spa_props_register (&props, "wp-test-property", "Test property",
+      wp_spa_pod_new_string ("default value"));
 
-  g_assert_cmpint (wp_spa_props_store (&props, SPA_PROP_volume,
-          SPA_POD_Float (0.8)), ==, 1);
-  g_assert_cmpint (wp_spa_props_store (&props, SPA_PROP_START_CUSTOM + 1,
-          SPA_POD_String ("test value")), ==, 1);
+  g_autoptr (WpSpaPod) new_float = wp_spa_pod_new_float (0.8);
+  g_autoptr (WpSpaPod) new_str = wp_spa_pod_new_string ("test value");
+  g_assert_true (wp_spa_props_store (&props, "volume", new_float));
+  g_assert_true (wp_spa_props_store (&props, "wp-test-property", new_str));
 
-  arr = wp_spa_props_build_all_pods (&props, &b);
+  arr = wp_spa_props_build_all_pods (&props);
   g_assert_nonnull (arr);
   g_assert_cmpint (arr->len, ==, 3);
 
   pod = g_ptr_array_index (arr, 0);
   g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_Props));
-  g_assert_true (spa_pod_is_object_id (pod, SPA_PARAM_Props));
-  g_assert_cmpint (spa_pod_parse_object (pod,
-          SPA_TYPE_OBJECT_Props, NULL,
-          SPA_PROP_volume, SPA_POD_Float (&float_value),
-          SPA_PROP_START_CUSTOM + 1, SPA_POD_String (&string_value)),
-      ==, 2);
+  g_assert_true (wp_spa_pod_get_object (pod,
+      "Props", &id_name,
+      "volume", "f", &float_value,
+      "wp-test-property", "s", &string_value,
+      NULL));
   g_assert_cmpfloat_with_epsilon (float_value, 0.8, 0.001);
   g_assert_cmpstr (string_value, ==, "test value");
 
   pod = g_ptr_array_index (arr, 1);
   g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_PropInfo));
-  g_assert_true (spa_pod_is_object_id (pod, SPA_PARAM_PropInfo));
-
-  g_assert_cmpint (spa_pod_parse_object (pod,
-          SPA_TYPE_OBJECT_PropInfo, NULL,
-          SPA_PROP_INFO_id, SPA_POD_Id (&id),
-          SPA_PROP_INFO_name, SPA_POD_String (&string_value),
-          SPA_PROP_INFO_type, SPA_POD_Pod (&pod)),
-      ==, 3);
+  g_assert_true (wp_spa_pod_get_object (pod,
+      "PropInfo", &id_name,
+      "id", "I", &id,
+      "name", "s", &string_value,
+      "type", "P", &pod_value,
+      NULL));
   g_assert_cmpuint (id, ==, SPA_PROP_volume);
   g_assert_cmpstr (string_value, ==, "Volume");
-  g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_choice (pod));
-  g_assert_true (SPA_POD_CHOICE_VALUE_TYPE (pod) == SPA_TYPE_Float);
+  g_assert_nonnull (pod_value);
+  g_assert_true (wp_spa_pod_is_choice (pod_value));
 
   pod = g_ptr_array_index (arr, 2);
   g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_PropInfo));
-  g_assert_true (spa_pod_is_object_id (pod, SPA_PARAM_PropInfo));
-
-  g_assert_cmpint (spa_pod_parse_object (pod,
-          SPA_TYPE_OBJECT_PropInfo, NULL,
-          SPA_PROP_INFO_id, SPA_POD_Id (&id),
-          SPA_PROP_INFO_name, SPA_POD_String (&string_value),
-          SPA_PROP_INFO_type, SPA_POD_Pod (&pod)),
-      ==, 3);
-  g_assert_cmpuint (id, ==, SPA_PROP_START_CUSTOM + 1);
+  g_assert_true (wp_spa_pod_get_object (pod,
+      "PropInfo", &id_name,
+      "id", "I", &id,
+      "name", "s", &string_value,
+      "type", "P", &pod_value,
+      NULL));
+  g_assert_cmpuint (id, >, SPA_PROP_START_CUSTOM);
   g_assert_cmpstr (string_value, ==, "Test property");
-  g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_string (pod));
+  g_assert_nonnull (pod_value);
+  g_assert_true (wp_spa_pod_is_string (pod_value));
 
   wp_spa_props_clear (&props);
+
+  wp_spa_type_deinit ();
 }
 
 static void
 test_spa_props_store_from_props (void)
 {
+  wp_spa_type_init (TRUE);
+  g_assert_true (wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS, "Wp:Test:Property", "wp-test-property"));
+
   WpSpaProps props = {0};
-  gchar buffer[512];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buffer, sizeof (buffer));
-  const struct spa_pod *pod;
+  g_autoptr (WpSpaPod) pod = NULL;
   float float_value = 0.0;
   const gchar *string_value = NULL;
-  g_autoptr (GArray) arr = g_array_new (FALSE, FALSE, sizeof (guint32));
-
-  wp_spa_props_register (&props, SPA_PROP_volume, "Volume",
-      SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0));
-  wp_spa_props_register (&props, SPA_PROP_START_CUSTOM + 1, "Test property",
-      SPA_POD_String ("default value"));
-
-  pod = spa_pod_builder_add_object (&b,
-      SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
-      SPA_PROP_volume, SPA_POD_Float (0.8),
-      SPA_PROP_START_CUSTOM + 1, SPA_POD_String ("test value"));
-
-  g_assert_cmpint (wp_spa_props_store_from_props (&props, pod, arr), ==, 2);
+  g_autoptr (GPtrArray) arr = g_ptr_array_new_with_free_func (g_free);
+
+  wp_spa_props_register (&props, "volume", "Volume",
+      wp_spa_pod_new_choice ("Range", "f", 1.0, "f", 0.0, "f", 10.0, NULL));
+  wp_spa_props_register (&props, "wp-test-property", "Test property",
+      wp_spa_pod_new_string ("default value"));
+
+  pod = wp_spa_pod_new_object (
+      "Props", "Props",
+      "volume", "f", 0.8,
+      "wp-test-property", "s", "test value",
+      NULL);
+  g_assert_nonnull (pod);
+  g_assert_true (wp_spa_props_store_from_props (&props, pod, arr));
   g_assert_cmpint (arr->len, ==, 2);
-  g_assert_cmpint (((guint32 *)arr->data)[0], ==, SPA_PROP_volume);
-  g_assert_cmpint (((guint32 *)arr->data)[1], ==, SPA_PROP_START_CUSTOM + 1);
+  g_assert_cmpstr ((const gchar *)g_ptr_array_index (arr, 0), ==, "volume");
+  g_assert_cmpstr ((const gchar *)g_ptr_array_index (arr, 1), ==, "wp-test-property");
 
-  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, SPA_PROP_volume));
-  g_assert_cmpint (spa_pod_get_float (pod, &float_value), ==, 0);
+  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, "volume"));
+  g_assert_true (wp_spa_pod_get_float (pod, &float_value));
   g_assert_cmpfloat_with_epsilon (float_value, 0.8, 0.001);
 
-  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, SPA_PROP_START_CUSTOM + 1));
-  g_assert_cmpint (spa_pod_get_string (pod, &string_value), ==, 0);
+  g_assert_nonnull (pod = wp_spa_props_get_stored (&props, "wp-test-property"));
+  g_assert_true (wp_spa_pod_get_string (pod, &string_value));
   g_assert_cmpstr (string_value, ==, "test value");
 
   wp_spa_props_clear (&props);
+
+  wp_spa_type_deinit ();
 }
 
 static void
 test_spa_props_register_from_prop_info (void)
 {
+  wp_spa_type_init (TRUE);
+  g_assert_true (wp_spa_type_register (WP_SPA_TYPE_TABLE_PROPS, "Wp:Test:Property", "wp-test-property"));
+  guint test_property_id = 0;
+  wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PROPS, "wp-test-property", &test_property_id, NULL, NULL);
+
   WpSpaProps props = {0};
-  gchar buffer[512];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buffer, sizeof (buffer));
-  const struct spa_pod *pod;
+  g_autoptr (WpSpaPod) prop_info = NULL;
+  WpSpaPod *pod = NULL;
   float float_value = 0.0;
   const gchar *string_value = NULL;
+  g_autoptr (WpSpaPod) pod_value = NULL;
   g_autoptr (GPtrArray) arr = NULL;
+  const gchar *id_name;
   guint32 id;
 
-  pod = spa_pod_builder_add_object (&b,
-      SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
-      SPA_PROP_INFO_id, SPA_POD_Id (SPA_PROP_volume),
-      SPA_PROP_INFO_name, SPA_POD_String ("Volume"),
-      SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0));
-
-  g_assert_cmpint (wp_spa_props_register_from_prop_info (&props, pod), ==, 0);
-
-  pod = spa_pod_builder_add_object (&b,
-      SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo,
-      SPA_PROP_INFO_id, SPA_POD_Id (SPA_PROP_START_CUSTOM + 1),
-      SPA_PROP_INFO_name, SPA_POD_String ("Test property"),
-      SPA_PROP_INFO_type, SPA_POD_String ("default value"));
-
-  g_assert_cmpint (wp_spa_props_register_from_prop_info (&props, pod), ==, 0);
-
-  g_assert_cmpint (wp_spa_props_store (&props, SPA_PROP_volume,
-          SPA_POD_Float (0.8)), ==, 1);
-  g_assert_cmpint (wp_spa_props_store (&props, SPA_PROP_START_CUSTOM + 1,
-          SPA_POD_String ("test value")), ==, 1);
-
-  arr = wp_spa_props_build_all_pods (&props, &b);
+  prop_info = wp_spa_pod_new_object (
+      "PropInfo", "PropInfo",
+      "id", "I", SPA_PROP_volume,
+      "name", "s", "Volume",
+      "type", SPA_POD_CHOICE_RANGE_Float (1.0, 0.0, 10.0),
+      NULL);
+  g_assert_nonnull (prop_info);
+  g_assert_true (wp_spa_props_register_from_prop_info (&props, prop_info));
+
+  prop_info = wp_spa_pod_new_object (
+      "PropInfo", "PropInfo",
+      "id", "I", test_property_id,
+      "name", "s", "Test property",
+      "type", "s", "default value",
+      NULL);
+  g_assert_nonnull (prop_info);
+  g_assert_true (wp_spa_props_register_from_prop_info (&props, prop_info));
+
+  g_autoptr (WpSpaPod) float_pod = wp_spa_pod_new_float (0.8);
+  g_autoptr (WpSpaPod) string_pod = wp_spa_pod_new_string ("test value");
+  g_assert_true (wp_spa_props_store (&props, "volume", float_pod));
+  g_assert_true (wp_spa_props_store (&props, "wp-test-property", string_pod));
+
+  arr = wp_spa_props_build_all_pods (&props);
   g_assert_nonnull (arr);
   g_assert_cmpint (arr->len, ==, 3);
 
   pod = g_ptr_array_index (arr, 0);
   g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_Props));
-  g_assert_true (spa_pod_is_object_id (pod, SPA_PARAM_Props));
-  g_assert_cmpint (spa_pod_parse_object (pod,
-          SPA_TYPE_OBJECT_Props, NULL,
-          SPA_PROP_volume, SPA_POD_Float (&float_value),
-          SPA_PROP_START_CUSTOM + 1, SPA_POD_String (&string_value)),
-      ==, 2);
+  g_assert_true (wp_spa_pod_get_object (pod,
+      "Props", &id_name,
+      "volume", "f", &float_value,
+      "wp-test-property", "s", &string_value,
+      NULL));
   g_assert_cmpfloat_with_epsilon (float_value, 0.8, 0.001);
   g_assert_cmpstr (string_value, ==, "test value");
 
   pod = g_ptr_array_index (arr, 1);
   g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_PropInfo));
-  g_assert_true (spa_pod_is_object_id (pod, SPA_PARAM_PropInfo));
-
-  g_assert_cmpint (spa_pod_parse_object (pod,
-          SPA_TYPE_OBJECT_PropInfo, NULL,
-          SPA_PROP_INFO_id, SPA_POD_Id (&id),
-          SPA_PROP_INFO_name, SPA_POD_String (&string_value),
-          SPA_PROP_INFO_type, SPA_POD_Pod (&pod)),
-      ==, 3);
+  g_assert_true (wp_spa_pod_get_object (pod,
+      "PropInfo", &id_name,
+      "id", "I", &id,
+      "name", "s", &string_value,
+      "type", "P", &pod_value,
+      NULL));
   g_assert_cmpuint (id, ==, SPA_PROP_volume);
   g_assert_cmpstr (string_value, ==, "Volume");
-  g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_choice (pod));
-  g_assert_true (SPA_POD_CHOICE_VALUE_TYPE (pod) == SPA_TYPE_Float);
+  g_assert_nonnull (pod_value);
+  g_assert_true (wp_spa_pod_is_choice (pod_value));
 
   pod = g_ptr_array_index (arr, 2);
   g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_object_type (pod, SPA_TYPE_OBJECT_PropInfo));
-  g_assert_true (spa_pod_is_object_id (pod, SPA_PARAM_PropInfo));
-
-  g_assert_cmpint (spa_pod_parse_object (pod,
-          SPA_TYPE_OBJECT_PropInfo, NULL,
-          SPA_PROP_INFO_id, SPA_POD_Id (&id),
-          SPA_PROP_INFO_name, SPA_POD_String (&string_value),
-          SPA_PROP_INFO_type, SPA_POD_Pod (&pod)),
-      ==, 3);
-  g_assert_cmpuint (id, ==, SPA_PROP_START_CUSTOM + 1);
+  g_assert_true (wp_spa_pod_get_object (pod,
+      "PropInfo", &id_name,
+      "id", "I", &id,
+      "name", "s", &string_value,
+      "type", "P", &pod_value,
+      NULL));
+  g_assert_cmpuint (id, ==, test_property_id);
   g_assert_cmpstr (string_value, ==, "Test property");
-  g_assert_nonnull (pod);
-  g_assert_true (spa_pod_is_string (pod));
+  g_assert_nonnull (pod_value);
+  g_assert_true (wp_spa_pod_is_string (pod_value));
 
   wp_spa_props_clear (&props);
+
+  wp_spa_type_deinit ();
 }
 
 int
diff --git a/tools/wireplumber-cli.c b/tools/wireplumber-cli.c
index 83e245f7..3ade4549 100644
--- a/tools/wireplumber-cli.c
+++ b/tools/wireplumber-cli.c
@@ -38,16 +38,19 @@ async_quit (WpCore *core, GAsyncResult *res, struct WpCliData * d)
 }
 
 static void
-print_dev_endpoint (WpEndpoint *ep, WpSession *session, WpDefaultEndpointType type)
+print_dev_endpoint (WpEndpoint *ep, WpSession *session, const gchar *type_name)
 {
   guint32 id = wp_proxy_get_bound_id (WP_PROXY (ep));
-  gboolean is_default = (session && type != 0 &&
-          wp_session_get_default_endpoint (session, type) == id);
+  gboolean is_default = (session && type_name != NULL &&
+          wp_session_get_default_endpoint (session, type_name) == id);
+  g_autoptr (WpSpaPod) ctrl = NULL;
   gfloat volume = 0.0;
   gboolean mute = FALSE;
 
-  wp_endpoint_get_control_float (ep, WP_ENDPOINT_CONTROL_VOLUME, &volume);
-  wp_endpoint_get_control_boolean (ep, WP_ENDPOINT_CONTROL_MUTE, &mute);
+  ctrl = wp_endpoint_get_control (ep, "volume");
+  wp_spa_pod_get_float (ctrl, &volume);
+  ctrl = wp_endpoint_get_control (ep, "mute");
+  wp_spa_pod_get_boolean (ctrl, &mute);
 
   g_print (" %c %4u. %60s\tvol: %.2f %s\n", is_default ? '*' : ' ', id,
       wp_endpoint_get_name (ep), volume, mute ? "MUTE" : "");
@@ -87,7 +90,7 @@ list_endpoints (WpObjectManager * om, struct WpCliData * d)
   {
     WpEndpoint *ep = g_value_get_object (&val);
     if (g_strcmp0 (wp_endpoint_get_media_class (ep), "Audio/Source") == 0)
-      print_dev_endpoint (ep, session, WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE);
+      print_dev_endpoint (ep, session, "wp-session-default-endpoint-audio-source");
   }
 
   wp_iterator_reset (it);
@@ -99,7 +102,7 @@ list_endpoints (WpObjectManager * om, struct WpCliData * d)
   {
     WpEndpoint *ep = g_value_get_object (&val);
     if (g_strcmp0 (wp_endpoint_get_media_class (ep), "Audio/Sink") == 0)
-      print_dev_endpoint (ep, session, WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK);
+      print_dev_endpoint (ep, session, "wp-session-default-endpoint-audio-sink");
   }
 
   wp_iterator_reset (it);
@@ -151,18 +154,18 @@ set_default (WpObjectManager * om, struct WpCliData * d)
     guint32 id = wp_proxy_get_bound_id (WP_PROXY (ep));
 
     if (id == d->params.set_default.id) {
-      WpDefaultEndpointType type;
+      const gchar * type_name;
       if (g_strcmp0 (wp_endpoint_get_media_class (ep), "Audio/Sink") == 0)
-        type = WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK;
+        type_name = "wp-session-default-endpoint-audio-sink";
       else if (g_strcmp0 (wp_endpoint_get_media_class (ep), "Audio/Source") == 0)
-        type = WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE;
+        type_name = "wp-session-default-endpoint-audio-source";
       else {
         g_print ("%u: not a device endpoint\n", id);
         g_main_loop_quit (d->loop);
         return;
       }
 
-      wp_session_set_default_endpoint (session, type, id);
+      wp_session_set_default_endpoint (session, type_name, id);
       wp_core_sync (d->core, NULL, (GAsyncReadyCallback) async_quit, d);
       return;
     }
@@ -185,8 +188,8 @@ set_volume (WpObjectManager * om, struct WpCliData * d)
     guint32 id = wp_proxy_get_bound_id (WP_PROXY (ep));
 
     if (id == d->params.set_volume.id) {
-      wp_endpoint_set_control_float (ep, WP_ENDPOINT_CONTROL_VOLUME,
-          d->params.set_volume.volume);
+      g_autoptr (WpSpaPod) vol = wp_spa_pod_new_float (d->params.set_volume.volume);
+      wp_endpoint_set_control (ep, "volume", vol);
       wp_core_sync (d->core, NULL, (GAsyncReadyCallback) async_quit, d);
       return;
     }
@@ -274,6 +277,18 @@ main (gint argc, gchar **argv)
 
   g_log_set_writer_func (wp_log_writer_default, NULL, NULL);
 
+  /* Register custom wireplumber session types */
+  wp_spa_type_init (TRUE);
+  wp_spa_type_register (WP_SPA_TYPE_TABLE_BASIC,
+      "Wp:Session:Default:Endpoint:Audio:Source",
+      "wp-session-default-endpoint-audio-source");
+  wp_spa_type_register (WP_SPA_TYPE_TABLE_BASIC,
+      "Wp:Session:Default:Endpoint:Audio:Sink",
+      "wp-session-default-endpoint-audio-sink");
+  wp_spa_type_register (WP_SPA_TYPE_TABLE_BASIC,
+      "Wp:Session:Default:Endpoint:Video:Source",
+      "wp-session-default-endpoint-video-source");
+
   context = g_option_context_new ("- PipeWire Session/Policy Manager Helper CLI");
   g_option_context_add_main_entries (context, entries, NULL);
   g_option_context_set_description (context, usage_string);
-- 
GitLab