diff --git a/include/sound/pcm.h b/include/sound/pcm.h
index ac8f3aef9205..7e635b3103a2 100644
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -482,6 +482,7 @@ struct snd_pcm_substream {
 	struct list_head link_list;	/* linked list member */
 	struct snd_pcm_group self_group;	/* fake group for non linked substream (with substream lock inside) */
 	struct snd_pcm_group *group;		/* pointer to current group */
+	struct list_head cpcm_candidate_node;
 	/* -- assigned files -- */
 	int ref_count;
 	atomic_t mmap_count;
diff --git a/include/sound/soc.h b/include/sound/soc.h
index e844f6afc5b5..32a6b5092192 100644
--- a/include/sound/soc.h
+++ b/include/sound/soc.h
@@ -426,6 +426,7 @@ struct snd_jack;
 struct snd_soc_card;
 struct snd_soc_pcm_stream;
 struct snd_soc_ops;
+struct snd_soc_condpcm;
 struct snd_soc_pcm_runtime;
 struct snd_soc_dai;
 struct snd_soc_dai_driver;
@@ -1154,6 +1155,64 @@ struct snd_soc_card {
 #define for_each_card_widgets_safe(card, w, _w)	\
 	list_for_each_entry_safe(w, _w, &card->widgets, list)
 
+/* Conditional PCM operations called by soc-pcm.c. */
+struct snd_soc_condpcm_ops {
+	int (*match)(struct snd_soc_condpcm *, struct snd_pcm_substream *,
+		     struct snd_pcm_substream *);
+	int (*hw_params)(struct snd_soc_condpcm *, struct snd_pcm_hw_params *);
+	int (*hw_free)(struct snd_soc_condpcm *);
+	int (*prepare)(struct snd_soc_condpcm *, struct snd_pcm_substream *);
+	int (*trigger)(struct snd_soc_condpcm *, struct snd_pcm_substream *, int);
+};
+
+/**
+ * struct snd_soc_condpcm_pred - Predicate, describes source or sink (substream)
+ *                               dependency for given conditional PCM.
+ *
+ * @card_name: Name of card owning substream to find.
+ * @link_name: Name of DAI LINK owning substream to find.
+ * @direction: Whether its SNDRV_PCM_STREAM_PLAYBACK or CAPTURE.
+ */
+struct snd_soc_condpcm_pred {
+	const char *card_name;
+	const char *link_name;
+	int direction;
+};
+
+/**
+ * struct snd_soc_condpcm - Conditional PCM descriptor.
+ *
+ * @ops: custom PCM operations.
+ * @preds: predicates for identifying source and sink for given conditional PCM.
+ *
+ * @source: substreaming acting as a data source, assigned at runtime.
+ * @sink: substreaming acting as a data sink, assigned at runtime.
+ * @state: current runtime state.
+ *
+ * @source_node: list navigation for rtd->source_list.
+ * @sink_node: list navigation for rtd->sink_list.
+ * @node: list navigation for condpcm_list (soc-condpcm.c).
+ */
+struct snd_soc_condpcm {
+	const struct snd_soc_condpcm_ops *ops;
+	struct snd_soc_condpcm_pred preds[2];
+
+	struct snd_pcm_substream *source;
+	struct snd_pcm_substream *sink;
+	snd_pcm_state_t state;
+	void *private_data;
+
+	/* Condpcms navigation for the pcm runtime. */
+	struct list_head source_node;
+	struct list_head sink_node;
+	struct list_head node;
+};
+
+int snd_soc_unregister_condpcm(struct snd_soc_condpcm *cpcm);
+struct snd_soc_condpcm *snd_soc_register_condpcm(struct snd_soc_condpcm_pred *source,
+						 struct snd_soc_condpcm_pred *sink,
+						 const struct snd_soc_condpcm_ops *ops,
+						 void *private_data);
 
 static inline int snd_soc_card_is_instantiated(struct snd_soc_card *card)
 {
@@ -1161,6 +1220,10 @@ static inline int snd_soc_card_is_instantiated(struct snd_soc_card *card)
 }
 
 /* SoC machine DAI configuration, glues a codec and cpu DAI together */
+/*
+ * @source_cpcm_list:   List of condpcms this pcm is source for.
+ * @sink_cpcm_list:     List of condpcms this pcm is sink for.
+ */
 struct snd_soc_pcm_runtime {
 	struct device *dev;
 	struct snd_soc_card *card;
@@ -1172,6 +1235,8 @@ struct snd_soc_pcm_runtime {
 	/* Dynamic PCM BE runtime data */
 	struct snd_soc_dpcm_runtime dpcm[SNDRV_PCM_STREAM_LAST + 1];
 	struct snd_soc_dapm_widget *c2c_widget[SNDRV_PCM_STREAM_LAST + 1];
+	struct list_head cpcm_source_list;
+	struct list_head cpcm_sink_list;
 
 	long pmdown_time;
 
diff --git a/sound/core/pcm.c b/sound/core/pcm.c
index dc37f3508dc7..12243cecaa11 100644
--- a/sound/core/pcm.c
+++ b/sound/core/pcm.c
@@ -663,6 +663,7 @@ int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count)
 		substream->pstr = pstr;
 		substream->number = idx;
 		substream->stream = stream;
+		INIT_LIST_HEAD(&substream->cpcm_candidate_node);
 		sprintf(substream->name, "subdevice #%i", idx);
 		substream->buffer_bytes_max = UINT_MAX;
 		if (prev == NULL)
diff --git a/sound/soc/Makefile b/sound/soc/Makefile
index 775bb38c2ed4..8004de7c500c 100644
--- a/sound/soc/Makefile
+++ b/sound/soc/Makefile
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0
 snd-soc-core-y := soc-core.o soc-dapm.o soc-jack.o soc-utils.o soc-dai.o soc-component.o
-snd-soc-core-y += soc-pcm.o soc-devres.o soc-ops.o soc-link.o soc-card.o
+snd-soc-core-y += soc-pcm.o soc-devres.o soc-ops.o soc-link.o soc-card.o soc-condpcm.o
 snd-soc-core-$(CONFIG_SND_SOC_COMPRESS) += soc-compress.o
 
 ifneq ($(CONFIG_SND_SOC_TOPOLOGY),)
diff --git a/sound/soc/soc-condpcm.c b/sound/soc/soc-condpcm.c
new file mode 100644
index 000000000000..786b3fafd714
--- /dev/null
+++ b/sound/soc/soc-condpcm.c
@@ -0,0 +1,348 @@
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// Conditional-PCM management
+//
+// Copyright(c) 2024 Intel Corporation
+//
+// Author: Cezary Rojewski <cezary.rojewski@intel.com>
+//
+
+#include <linux/export.h>
+#include <linux/list.h>
+#include <linux/slab.h>
+#include <sound/pcm.h>
+#include <sound/soc.h>
+#include "soc-condpcm.h"
+
+/*
+ * condpcm_list - Stores all registered conditional pcms.
+ * condpcm_candidate_list - Stores all substreams that passed hw_params() step.
+ */
+static LIST_HEAD(condpcm_list);
+static LIST_HEAD(condpcm_candidate_list);
+
+static bool dpcm_prepare_done(const struct snd_pcm_substream *substream)
+{
+	struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
+	enum snd_soc_dpcm_state state = rtd->dpcm[substream->stream].state;
+
+	return state == SND_SOC_DPCM_STATE_PREPARE ||
+	       state == SND_SOC_DPCM_STATE_START ||
+	       state == SND_SOC_DPCM_STATE_PAUSED;
+}
+
+static bool dpcm_hw_params_done(const struct snd_pcm_substream *substream)
+{
+	struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
+	enum snd_soc_dpcm_state state = rtd->dpcm[substream->stream].state;
+
+	return state >= SND_SOC_DPCM_STATE_HW_PARAMS &&
+	       state < SND_SOC_DPCM_STATE_HW_FREE;
+}
+
+static bool dpcm_start_done(const struct snd_pcm_substream *substream)
+{
+	struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
+	enum snd_soc_dpcm_state state = rtd->dpcm[substream->stream].state;
+
+	return state == SND_SOC_DPCM_STATE_START ||
+	       state == SND_SOC_DPCM_STATE_PAUSED;
+}
+
+static int soc_condpcm_hw_params(struct snd_soc_condpcm *cpcm,
+				 struct snd_pcm_hw_params *params)
+{
+	struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(cpcm->source);
+	struct snd_soc_pcm_runtime *rtd2 = snd_soc_substream_to_rtd(cpcm->sink);
+	int ret;
+
+	ret = cpcm->ops->hw_params(cpcm, params);
+	if (ret)
+		return ret;
+
+	list_add_tail(&cpcm->source_node, &rtd->cpcm_source_list);
+	list_add_tail(&cpcm->sink_node, &rtd2->cpcm_sink_list);
+	cpcm->state = SNDRV_PCM_STATE_SETUP;
+	return 0;
+}
+
+static void soc_condpcm_hw_free(struct snd_soc_condpcm *cpcm)
+{
+	cpcm->ops->hw_free(cpcm);
+	list_del(&cpcm->source_node);
+	list_del(&cpcm->sink_node);
+	cpcm->state = SNDRV_PCM_STATE_DISCONNECTED;
+}
+
+static void soc_condpcm_prepare(struct snd_soc_condpcm *cpcm,
+				struct snd_pcm_substream *substream)
+{
+	int ret;
+
+	ret = cpcm->ops->prepare(cpcm, substream);
+	if (!ret)
+		cpcm->state = SNDRV_PCM_STATE_PREPARED;
+}
+
+static void soc_condpcm_start(struct snd_soc_condpcm *cpcm,
+			      struct snd_pcm_substream *substream, int cmd)
+{
+	int ret;
+
+	ret = cpcm->ops->trigger(cpcm, substream, cmd);
+	if (ret)
+		cpcm->state = SNDRV_PCM_STATE_SETUP;
+	else
+		cpcm->state = SNDRV_PCM_STATE_RUNNING;
+}
+
+static void soc_condpcm_stop(struct snd_soc_condpcm *cpcm,
+			     struct snd_pcm_substream *substream, int cmd)
+{
+	int ret;
+
+	ret = cpcm->ops->trigger(cpcm, substream, cmd);
+	if (ret || cmd != SNDRV_PCM_TRIGGER_PAUSE_PUSH)
+		cpcm->state = SNDRV_PCM_STATE_SETUP;
+	else
+		cpcm->state = SNDRV_PCM_STATE_PREPARED;
+}
+
+void snd_soc_condpcms_prepare(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream)
+{
+	struct snd_soc_condpcm *cpcm;
+
+	/* Prepare conditional pcms only if source and sink are both preprared. */
+	list_for_each_entry(cpcm, &rtd->cpcm_source_list, source_node) {
+		if (cpcm->state == SNDRV_PCM_STATE_SETUP &&
+		    dpcm_prepare_done(cpcm->sink))
+			soc_condpcm_prepare(cpcm, substream);
+	}
+
+	list_for_each_entry(cpcm, &rtd->cpcm_sink_list, sink_node) {
+		if (cpcm->state == SNDRV_PCM_STATE_SETUP &&
+		    dpcm_prepare_done(cpcm->source))
+			soc_condpcm_prepare(cpcm, substream);
+	}
+}
+
+static void soc_condpcms_start(struct snd_soc_pcm_runtime *rtd,
+			       struct snd_pcm_substream *substream, int cmd)
+{
+	struct snd_soc_condpcm *cpcm;
+
+	/* Start conditional pcms only if source and sink are both running. */
+	list_for_each_entry(cpcm, &rtd->cpcm_source_list, source_node) {
+		if (cpcm->state == SNDRV_PCM_STATE_PREPARED &&
+		    dpcm_start_done(cpcm->sink))
+			soc_condpcm_start(cpcm, substream, cmd);
+	}
+
+	list_for_each_entry(cpcm, &rtd->cpcm_sink_list, sink_node) {
+		if (cpcm->state == SNDRV_PCM_STATE_PREPARED &&
+		    dpcm_start_done(cpcm->source))
+			soc_condpcm_start(cpcm, substream, cmd);
+	}
+}
+
+static void soc_condpcms_stop(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream, int cmd)
+{
+	struct snd_soc_condpcm *cpcm;
+
+	/* Stop conditional pcms if either source or sink is not running. */
+	list_for_each_entry(cpcm, &rtd->cpcm_source_list, source_node)
+		if (cpcm->state == SNDRV_PCM_STATE_RUNNING)
+			soc_condpcm_stop(cpcm, substream, cmd);
+
+	list_for_each_entry(cpcm, &rtd->cpcm_sink_list, sink_node)
+		if (cpcm->state == SNDRV_PCM_STATE_RUNNING)
+			soc_condpcm_stop(cpcm, substream, cmd);
+}
+
+void snd_soc_condpcms_trigger(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream, int cmd)
+{
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_RESUME:
+	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+		soc_condpcms_start(rtd, substream, cmd);
+		break;
+	case SNDRV_PCM_TRIGGER_STOP:
+	case SNDRV_PCM_TRIGGER_SUSPEND:
+	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+		soc_condpcms_stop(rtd, substream, cmd);
+		break;
+	}
+}
+
+void snd_soc_condpcms_teardown(struct snd_soc_pcm_runtime *rtd,
+			       struct snd_pcm_substream *substream)
+{
+	struct snd_soc_condpcm *cpcm, *save;
+
+	list_del(&substream->cpcm_candidate_node);
+
+	/* Free all condpcms this substream spawned. */
+	list_for_each_entry_safe(cpcm, save, &rtd->cpcm_source_list, source_node)
+		soc_condpcm_hw_free(cpcm);
+	list_for_each_entry_safe(cpcm, save, &rtd->cpcm_sink_list, sink_node)
+		soc_condpcm_hw_free(cpcm);
+}
+
+static bool soc_condpcm_test(struct snd_soc_condpcm *cpcm,
+			     struct snd_pcm_substream *substream, int dir)
+{
+	struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
+	struct snd_soc_condpcm_pred *pred = &cpcm->preds[dir];
+
+	return pred->direction == substream->stream &&
+	       !strcmp(pred->card_name, rtd->card->name) &&
+	       !strcmp(pred->link_name, rtd->dai_link->name);
+}
+
+static struct snd_pcm_substream *
+soc_condpcm_find_match(struct snd_soc_condpcm *cpcm, struct snd_pcm_substream *ignore, int dir)
+{
+	struct snd_pcm_substream *substream;
+
+	list_for_each_entry(substream, &condpcm_candidate_list, cpcm_candidate_node) {
+		if (substream == ignore)
+			continue;
+
+		/* Only substreams that passed hw_params() are valid candidates. */
+		if (!dpcm_hw_params_done(substream))
+			continue;
+
+		if (soc_condpcm_test(cpcm, substream, dir))
+			return substream;
+	}
+
+	return NULL;
+}
+
+static int soc_condpcm_walk(struct snd_soc_pcm_runtime *rtd,
+			    struct snd_pcm_substream *substream,
+			    struct snd_pcm_hw_params *params, int dir)
+{
+	/* Temporary source/sink cache. */
+	struct snd_pcm_substream *substreams[2];
+	struct snd_soc_condpcm *cpcm;
+	int ret;
+
+	substreams[dir] = substream;
+
+	list_for_each_entry(cpcm, &condpcm_list, node) {
+		if (cpcm->state != SNDRV_PCM_STATE_DISCONNECTED)
+			continue;
+
+		/* Does this cpcm match @substream? */
+		if (!soc_condpcm_test(cpcm, substream, dir))
+			continue;
+
+		/* Find pair for the @substream. */
+		substreams[!dir] = soc_condpcm_find_match(cpcm, substream, !dir);
+		if (!substreams[!dir])
+			continue;
+
+		/* Allow driver to have the final word. */
+		ret = cpcm->ops->match(cpcm, substreams[0], substreams[1]);
+		if (ret)
+			continue;
+		cpcm->source = substreams[0];
+		cpcm->sink = substreams[1];
+
+		ret = soc_condpcm_hw_params(cpcm, params);
+		if (ret) {
+			cpcm->source = NULL;
+			cpcm->sink = NULL;
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+/* Called by soc-pcm.c after each successful hw_params(). */
+int snd_soc_condpcms_walk_all(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream,
+			      struct snd_pcm_hw_params *params)
+{
+	int ret;
+
+	list_add_tail(&substream->cpcm_candidate_node, &condpcm_candidate_list);
+
+	/* Spawn all condpcms this substream is the missing source of. */
+	ret = soc_condpcm_walk(rtd, substream, params, SNDRV_PCM_STREAM_CAPTURE);
+	if (ret)
+		return ret;
+
+	/* Spawn all condpcms this substream is the missing sink of. */
+	return soc_condpcm_walk(rtd, substream, params, SNDRV_PCM_STREAM_PLAYBACK);
+}
+
+/**
+ * snd_soc_unregister_condpcm - unregister conditional PCM from ASoC framework.
+ *
+ * @cpcm - the instance to unregister and free
+ *
+ * Return: Zero on success or -EBUSY on if the instance is in-use.
+ */
+int snd_soc_unregister_condpcm(struct snd_soc_condpcm *cpcm)
+{
+	if (cpcm->state != SNDRV_PCM_STATE_DISCONNECTED)
+		return -EBUSY;
+
+	list_del(&cpcm->node);
+	kfree(cpcm);
+	return 0;
+}
+EXPORT_SYMBOL_GPL(snd_soc_unregister_condpcm);
+
+/**
+ * snd_soc_register_condpcm - register a conditional PCM to ASoC framework.
+ * @source:		predicate for identifying the data source (substream).
+ * @sink:		predicate for identifying the data sink (substream).
+ * @ops:		driver specific PCM operations.
+ * @private_data:	driver private context.
+ *
+ * Return: A newly created PCM instance or ERR_PTR() on failure.
+ *
+ * The caller is responsible for freeing the instance with
+ * snd_soc_unregister_condpcm() when no longer of use.
+ */
+struct snd_soc_condpcm *snd_soc_register_condpcm(struct snd_soc_condpcm_pred *source,
+						 struct snd_soc_condpcm_pred *sink,
+						 const struct snd_soc_condpcm_ops *ops,
+						 void *private_data)
+{
+	struct snd_soc_condpcm *cpcm;
+
+	/* Sanity check. */
+	if (!ops->match || !ops->hw_params || !ops->hw_free || !ops->prepare || !ops->trigger)
+		return ERR_PTR(-EINVAL);
+
+	cpcm = kzalloc(sizeof(*cpcm), GFP_KERNEL);
+	if (!cpcm)
+		return ERR_PTR(-ENOMEM);
+
+	cpcm->ops = ops;
+	cpcm->preds[0].card_name = source->card_name;
+	cpcm->preds[0].link_name = source->link_name;
+	cpcm->preds[0].direction = source->direction;
+	cpcm->preds[1].card_name = sink->card_name;
+	cpcm->preds[1].link_name = sink->link_name;
+	cpcm->preds[1].direction = sink->direction;
+	cpcm->state = SNDRV_PCM_STATE_DISCONNECTED;
+	cpcm->private_data = private_data;
+	INIT_LIST_HEAD(&cpcm->source_node);
+	INIT_LIST_HEAD(&cpcm->sink_node);
+	INIT_LIST_HEAD(&cpcm->node);
+
+	list_add_tail(&cpcm->node, &condpcm_list);
+
+	return cpcm;
+}
+EXPORT_SYMBOL_GPL(snd_soc_register_condpcm);
diff --git a/sound/soc/soc-condpcm.h b/sound/soc/soc-condpcm.h
new file mode 100644
index 000000000000..65df13af7d44
--- /dev/null
+++ b/sound/soc/soc-condpcm.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef __SND_SOC_CONDPCM_H
+#define __SND_SOC_CONDPCM_H
+
+void snd_soc_condpcms_prepare(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream);
+void snd_soc_condpcms_trigger(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream, int cmd);
+
+void snd_soc_condpcms_teardown(struct snd_soc_pcm_runtime *rtd,
+			       struct snd_pcm_substream *substream);
+int snd_soc_condpcms_walk_all(struct snd_soc_pcm_runtime *rtd,
+			      struct snd_pcm_substream *substream,
+			      struct snd_pcm_hw_params *params);
+
+#endif
diff --git a/sound/soc/soc-core.c b/sound/soc/soc-core.c
index 20248a29d167..aa595a83c926 100644
--- a/sound/soc/soc-core.c
+++ b/sound/soc/soc-core.c
@@ -532,6 +532,8 @@ static struct snd_soc_pcm_runtime *soc_new_pcm_runtime(
 
 	rtd->dev = dev;
 	INIT_LIST_HEAD(&rtd->list);
+	INIT_LIST_HEAD(&rtd->cpcm_source_list);
+	INIT_LIST_HEAD(&rtd->cpcm_sink_list);
 	for_each_pcm_streams(stream) {
 		INIT_LIST_HEAD(&rtd->dpcm[stream].be_clients);
 		INIT_LIST_HEAD(&rtd->dpcm[stream].fe_clients);
diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c
index d95a73b2ed03..e3d3e65a5f56 100644
--- a/sound/soc/soc-pcm.c
+++ b/sound/soc/soc-pcm.c
@@ -26,6 +26,8 @@
 #include <sound/soc-link.h>
 #include <sound/initval.h>
 
+#include "soc-condpcm.h"
+
 #define soc_pcm_ret(rtd, ret) _soc_pcm_ret(rtd, __func__, ret)
 static inline int _soc_pcm_ret(struct snd_soc_pcm_runtime *rtd,
 			       const char *func, int ret)
@@ -920,6 +922,8 @@ static int __soc_pcm_prepare(struct snd_soc_pcm_runtime *rtd,
 	}
 
 out:
+	if (!ret)
+		snd_soc_condpcms_prepare(rtd, substream);
 	return soc_pcm_ret(rtd, ret);
 }
 
@@ -954,6 +958,9 @@ static int soc_pcm_hw_clean(struct snd_soc_pcm_runtime *rtd,
 
 	snd_soc_dpcm_mutex_assert_held(rtd);
 
+	if (!rollback)
+		snd_soc_condpcms_teardown(rtd, substream);
+
 	/* clear the corresponding DAIs parameters when going to be inactive */
 	for_each_rtd_dais(rtd, i, dai) {
 		if (snd_soc_dai_active(dai) == 1)
@@ -1105,6 +1112,8 @@ static int __soc_pcm_hw_params(struct snd_soc_pcm_runtime *rtd,
 out:
 	if (ret < 0)
 		soc_pcm_hw_clean(rtd, substream, 1);
+	else
+		snd_soc_condpcms_walk_all(rtd, substream, params);
 
 	return soc_pcm_ret(rtd, ret);
 }
@@ -1210,6 +1219,8 @@ static int soc_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
 		}
 	}
 
+	if (!ret)
+		snd_soc_condpcms_trigger(rtd, substream, cmd);
 	return ret;
 }
 
