[RFC PATCH 02/23] ASoC: soc-pcm: add multi_direction_codecs link flag
From: Srinivas Kandagatla
Date: Fri Oct 23 2026 - 13:18:59 EST
Speaker protection needs the amplifiers' V/I sense capture running
whenever the speakers play. Putting the capture codec DAIs on the
playback BE link ties the two together, but the core skips codec DAIs
whose direction does not match the substream.
Add a multi_direction_codecs link flag. When set, the core also calls
.hw_params and .hw_free on codec DAIs of the other direction, with the
substream direction switched to theirs for the call under pcm_mutex,
and sends the matching stream events so their widgets power up and
down. An optional multi_direction_params_fixup callback lets the
machine driver set the parameters for those DAIs. Links without the
flag are unchanged.
Document the flag in Documentation/sound/soc/multi-direction-codecs.rst.
Signed-off-by: Srinivas Kandagatla <srinivas.kandagatla@xxxxxxxxxxxxxxxx>
---
Documentation/sound/soc/index.rst | 1 +
.../sound/soc/multi-direction-codecs.rst | 135 ++++++++++++++++++
include/sound/soc.h | 7 +
sound/soc/soc-pcm.c | 122 +++++++++++++---
4 files changed, 244 insertions(+), 21 deletions(-)
create mode 100644 Documentation/sound/soc/multi-direction-codecs.rst
diff --git a/Documentation/sound/soc/index.rst b/Documentation/sound/soc/index.rst
index 22878d73f7a5..62d01a0a6e52 100644
--- a/Documentation/sound/soc/index.rst
+++ b/Documentation/sound/soc/index.rst
@@ -18,4 +18,5 @@ The documentation is split into the following sections:-
jack
dpcm
codec-to-codec
+ multi-direction-codecs
usb
diff --git a/Documentation/sound/soc/multi-direction-codecs.rst b/Documentation/sound/soc/multi-direction-codecs.rst
new file mode 100644
index 000000000000..75befb7501b6
--- /dev/null
+++ b/Documentation/sound/soc/multi-direction-codecs.rst
@@ -0,0 +1,135 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+===========================
+Multi-direction codec DAIs
+===========================
+
+Overview
+========
+
+An ASoC backend (BE) link that bundles **codec DAIs of both directions**
+on a **single-direction CPU DAI** can set
+``snd_soc_dai_link.multi_direction_codecs`` to tell the ASoC core to
+call ``.hw_params`` / ``.hw_free`` on opposite-direction codec DAIs with
+their own direction, instead of skipping them.
+
+Typical use case: a smart-amp speaker protection link where the CPU
+side is a playback-only DMA (e.g. ``WSA_CODEC_DMA_RX_0`` on Qualcomm
+AudioReach), but the codec side exposes both a playback SPKR DAI and a
+companion capture V/I-sense DAI on the same SoundWire bus. Opening the
+playback FE should bring up both the amplifier output chain *and* its
+V/I feedback capture chain atomically, without userspace having to open
+a secondary PCM.
+
+Without the flag, the second codec DAI's ``.hw_params`` is skipped
+because its native direction does not match ``substream->stream``;
+``snd_soc_dai_stream_valid()`` filters it out. With the flag set, the
+ASoC core:
+
+1. Still iterates every codec DAI on the link.
+2. For a codec DAI whose native direction is **opposite** the
+ substream's, calls its ``.hw_params`` / ``.hw_free`` with its own
+ direction (by briefly flipping the substream direction under
+ ``pcm_mutex``, encapsulated in ``soc_pcm_dai_hw_params_at_stream``).
+3. Optionally calls ``.multi_direction_params_fixup`` so the machine
+ driver can rewrite rate/format/channels on a per-codec copy of the
+ params (an 8 kHz / S32_LE V/I-sense codec DAI will not accept a
+ 48 kHz / S16_LE playback stream's params verbatim).
+4. Sends start and stop stream events for the opposite direction in
+ prepare and hw_free, so the stream widgets of those codec DAIs power
+ up and down with the link.
+
+Links that do not set the flag retain the pre-existing behaviour
+exactly.
+
+When to use this
+================
+
+Prefer ``multi_direction_codecs`` when:
+
+* the two directions are physically **inseparable** on the codec side
+ (same chip, same wire); and
+* a single open semantically covers both (speaker protection, playback
+ with feedback for echo-reference, haptic/boost feedback); and
+* you do not want userspace to see or manage two PCMs.
+
+Prefer **DPCM FE/BE pairs** when:
+
+* the two directions can be exercised independently; or
+* userspace legitimately needs to control each direction (classic
+ playback + microphone capture).
+
+Prefer a **DAPM supply widget** when:
+
+* only the DAPM power graph (clock, LDO, bias) needs linking, and
+* no ``.hw_params`` plumbing is required on the secondary direction.
+
+Interaction with DAPM
+=====================
+
+DAPM edges are directional and the flag does not change the graph. The
+opposite-direction codec DAIs' stream widgets are powered by the extra
+start and stop stream events the core sends for that direction in
+prepare and hw_free.
+
+Only ``.hw_params`` and ``.hw_free`` are called on the opposite-direction
+codec DAIs. Their ``.startup``, ``.prepare``, ``.trigger``,
+``.shutdown`` and mute callbacks are not, so a codec DAI used this way
+must not depend on them.
+
+Machine-driver example
+======================
+
+A machine driver that bundles V/I-sense capture codec DAIs onto a
+speaker-playback BE link::
+
+ static int wsa_vi_params_fixup(struct snd_soc_pcm_runtime *rtd,
+ struct snd_pcm_hw_params *params,
+ int stream)
+ {
+ if (stream != SNDRV_PCM_STREAM_CAPTURE)
+ return 0;
+ /* VI capture chain runs at 8kHz / S32_LE / 4ch */
+ hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE)->min = 8000;
+ hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE)->max = 8000;
+ hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS)->min = 4;
+ hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS)->max = 4;
+ snd_mask_none(hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT));
+ snd_mask_set(hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT),
+ SNDRV_PCM_FORMAT_S32_LE);
+ return 0;
+ }
+
+ SND_SOC_DAILINK_DEFS(wsa_sp, ...);
+ SND_SOC_DAILINK_REG(wsa_sp);
+
+ static struct snd_soc_dai_link board_dai_links[] = {
+ {
+ .name = "WSA Playback",
+ .multi_direction_codecs = 1,
+ .multi_direction_params_fixup = wsa_vi_params_fixup,
+ SND_SOC_DAILINK_REG(wsa_sp),
+ /*
+ * codec DAIs include both the playback SPKR DAIs and
+ * their companion capture V/I-sense DAIs on the same
+ * SoundWire bus.
+ */
+ },
+ };
+
+Notes
+=====
+
+* ``multi_direction_params_fixup`` is called with the **flipped**
+ direction so the fixup logic can key off ``stream`` to decide
+ whether the params need rewriting.
+* ``snd_soc_dai_symmetric_set_params()`` runs after the flipped
+ ``.hw_params``. The symmetric-params tracking is per-DAI, so each
+ codec DAI records the format it was called with in its own
+ direction; nothing cross-contaminates.
+* The transient ``substream->stream`` flip is bounded to a single
+ codec DAI's ``.hw_params`` / ``.hw_free`` call and its DAPM update,
+ under ``pcm_mutex``. The codec DAI callbacks see the flipped value,
+ which is the purpose of the flip. ALSA core paths that do not take
+ ``pcm_mutex``, such as ``snd_pcm_info()``, can observe it during that
+ window.
diff --git a/include/sound/soc.h b/include/sound/soc.h
index f6b98324f346..f12fadb64268 100644
--- a/include/sound/soc.h
+++ b/include/sound/soc.h
@@ -749,6 +749,10 @@ struct snd_soc_dai_link {
int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_hw_params *params);
+ /* Adjust params for codec DAIs of the other direction */
+ int (*multi_direction_params_fixup)(struct snd_soc_pcm_runtime *rtd,
+ struct snd_pcm_hw_params *params, int stream);
+
/* machine stream operations */
const struct snd_soc_ops *ops;
const struct snd_soc_compr_ops *compr_ops;
@@ -783,6 +787,9 @@ struct snd_soc_dai_link {
/* This DAI link can route to other DAI links at runtime (Frontend)*/
unsigned int dynamic:1;
+ /* Also run codec DAIs of the other direction on this BE link */
+ unsigned int multi_direction_codecs:1;
+
/* DPCM used FE & BE merged format */
unsigned int dpcm_merged_format:1;
/* DPCM used FE & BE merged channel */
diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c
index ef8ccf52798e..5dbd0a78933a 100644
--- a/sound/soc/soc-pcm.c
+++ b/sound/soc/soc-pcm.c
@@ -986,6 +986,9 @@ static int __soc_pcm_prepare(struct snd_soc_pcm_runtime *rtd,
snd_soc_dapm_stream_event(rtd, substream->stream,
SND_SOC_DAPM_STREAM_START);
+ if (rtd->dai_link->multi_direction_codecs)
+ snd_soc_dapm_stream_event(rtd, substream->stream ^ 1,
+ SND_SOC_DAPM_STREAM_START);
for_each_rtd_dais(rtd, i, dai) {
if (!snd_soc_dai_mute_is_ctrled_at_trigger(dai))
@@ -1032,6 +1035,56 @@ static void soc_pcm_codec_params_fixup(struct snd_pcm_hw_params *params,
interval->max = channels;
}
+/*
+ * Call .hw_params on a codec DAI of the other direction, with the substream
+ * direction switched to the DAI's under pcm_mutex.
+ */
+static int soc_pcm_dai_hw_params_at_stream(struct snd_soc_dai *dai,
+ struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params,
+ int stream)
+{
+ int saved_stream = substream->stream;
+ int ret;
+
+ if (stream == saved_stream)
+ return snd_soc_dai_hw_params(dai, substream, params);
+
+ substream->stream = stream;
+ ret = snd_soc_dai_hw_params(dai, substream, params);
+ substream->stream = saved_stream;
+ return ret;
+}
+
+/* hw_free counterpart to soc_pcm_dai_hw_params_at_stream(). */
+static void soc_pcm_dai_hw_free_at_stream(struct snd_soc_dai *dai,
+ struct snd_pcm_substream *substream,
+ int rollback, int stream)
+{
+ int saved_stream = substream->stream;
+
+ if (stream == saved_stream) {
+ snd_soc_dai_hw_free(dai, substream, rollback);
+ return;
+ }
+
+ substream->stream = stream;
+ snd_soc_dai_hw_free(dai, substream, rollback);
+ substream->stream = saved_stream;
+}
+
+/* DAPM counterpart, so the DAI's own direction widget gets updated */
+static void soc_pcm_dapm_update_dai_at_stream(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *params,
+ struct snd_soc_dai *dai, int stream)
+{
+ int saved_stream = substream->stream;
+
+ substream->stream = stream;
+ snd_soc_dapm_update_dai(substream, params, dai);
+ substream->stream = saved_stream;
+}
+
static int soc_pcm_hw_clean(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_substream *substream, int rollback)
{
@@ -1053,6 +1106,9 @@ static int soc_pcm_hw_clean(struct snd_soc_pcm_runtime *rtd,
/* run the stream event */
snd_soc_dapm_stream_stop(rtd, substream->stream);
+ if (rtd->dai_link->multi_direction_codecs)
+ snd_soc_dapm_stream_event(rtd, substream->stream ^ 1,
+ SND_SOC_DAPM_STREAM_STOP);
/* free any machine hw params */
snd_soc_link_hw_free(substream, rollback);
@@ -1061,9 +1117,20 @@ static int soc_pcm_hw_clean(struct snd_soc_pcm_runtime *rtd,
snd_soc_pcm_component_hw_free(substream, rollback);
/* now free hw params for the DAIs */
- for_each_rtd_dais(rtd, i, dai)
- if (snd_soc_dai_stream_valid(dai, substream->stream))
- snd_soc_dai_hw_free(dai, substream, rollback);
+ for_each_rtd_dais(rtd, i, dai) {
+ int dai_stream = substream->stream;
+
+ if (!snd_soc_dai_stream_valid(dai, dai_stream)) {
+ if (!rtd->dai_link->multi_direction_codecs)
+ continue;
+ dai_stream ^= 1;
+ if (!snd_soc_dai_stream_valid(dai, dai_stream))
+ continue;
+ }
+
+ soc_pcm_dai_hw_free_at_stream(dai, substream, rollback,
+ dai_stream);
+ }
return 0;
}
@@ -1114,30 +1181,41 @@ static int __soc_pcm_hw_params(struct snd_pcm_substream *substream,
goto out;
for_each_rtd_codec_dais(rtd, i, codec_dai) {
- unsigned int ch_mask = snd_soc_dai_tdm_mask_get(codec_dai, substream->stream);
+ int codec_stream = substream->stream;
+ unsigned int ch_mask;
struct snd_soc_dai_link_ch_map *ch_maps;
int j;
/*
* Skip CODECs which don't support the current stream type,
- * the idea being that if a CODEC is not used for the currently
- * set up transfer direction, it should not need to be
- * configured, especially since the configuration used might
- * not even be supported by that CODEC. There may be cases
- * however where a CODEC needs to be set up although it is
- * actually not being used for the transfer, e.g. if a
- * capture-only CODEC is acting as an LRCLK and/or BCLK master
- * for the DAI link including a playback-only CODEC.
- * If this becomes necessary, we will have to augment the
- * machine driver setup with information on how to act, so
- * we can do the right thing here.
+ * unless the link runs them in their own direction.
*/
- if (!snd_soc_dai_stream_valid(codec_dai, substream->stream))
- continue;
+ bool direction_flipped = false;
+
+ if (!snd_soc_dai_stream_valid(codec_dai, codec_stream)) {
+ if (!rtd->dai_link->multi_direction_codecs)
+ continue;
+ codec_stream ^= 1;
+ if (!snd_soc_dai_stream_valid(codec_dai, codec_stream))
+ continue;
+ direction_flipped = true;
+ }
+
+ ch_mask = snd_soc_dai_tdm_mask_get(codec_dai, codec_stream);
/* copy params for each codec */
tmp_params = *params;
+ /* The substream params describe the other direction */
+ if (direction_flipped && rtd->dai_link->multi_direction_params_fixup) {
+ int fret = rtd->dai_link->multi_direction_params_fixup(
+ rtd, &tmp_params, codec_stream);
+ if (fret < 0) {
+ ret = fret;
+ goto out;
+ }
+ }
+
/* fixup params based on TDM or ch_map masks */
if (!ch_mask) {
for_each_rtd_ch_maps(rtd, j, ch_maps)
@@ -1148,13 +1226,15 @@ static int __soc_pcm_hw_params(struct snd_pcm_substream *substream,
if (ch_mask)
soc_pcm_codec_params_fixup(&tmp_params, ch_mask);
- ret = snd_soc_dai_hw_params(codec_dai, substream,
- &tmp_params);
- if(ret < 0)
+ ret = soc_pcm_dai_hw_params_at_stream(codec_dai, substream,
+ &tmp_params,
+ codec_stream);
+ if (ret < 0)
goto out;
snd_soc_dai_symmetric_set_params(codec_dai, &tmp_params);
- snd_soc_dapm_update_dai(substream, &tmp_params, codec_dai);
+ soc_pcm_dapm_update_dai_at_stream(substream, &tmp_params,
+ codec_dai, codec_stream);
}
for_each_rtd_cpu_dais(rtd, i, cpu_dai) {
--
2.47.3