Re: [PATCH] [RFC] ASoC: Conditional PCM support
On 8/21/24 12:18, Cezary Rojewski wrote:
Conditional PCM (condpcm) helps facilitate modern audio usecases such as Echo Cancellations and Noise Reduction. These are not invoked by the means of userspace application opening an endpoint (FrontEnd) but are a "side effect" of selected PCMs running simultaneously e.g.: if both Speaker (source) and Microphone Array (sink) are running, reference data from the Speaker and take it into account when processing capture for better voice command detection ratio.
The point about dependencies between capture/playback usages is certainly valid, and we've faced it multiple times for SOF - and even before in the mobile phone days. I am not convinced however that the graph management suggested here solves the well-known DPCM routing problems? See notes in no specific order below.
I am not following what the 'source' and 'sink' concepts refer to in this context. It looks like you are referring to regular PCM devices, i.e. Front Ends in soc-pcm parlance but examples and code make references to Back Ends.
There are also complicated cases where the amplifiers can provide an echo reference for AEC and I/V sensing for speaker protection. You would want to capture both even if there's no capture happening at the userspace level. This is a well-know DPCM routing issue where we have to rely on a Front-End being opened and some tags in UCM to deal with loose coupling.
It would help if you added precisions on your assumptions of where the processing takes place. In some cases Echo cancellation is handled in userspace, others in SOC firmware and others externally in a codec.
The notion of source/sink is also problematic when the same BE provides two sources of information that will be split, again same problem with amplifier feedback being used for two separate functions. What happens if you have multiple sinks for one source?
Same for the cases where the mic input is split multiple ways with different processing added on different PCM capture devices, e.g. for WebRTC there's an ask for a raw input, an AEC-processed input and AEC+NS-processed input. That's typically implemented with two splitters, the echo reference would be used by an intermediate node inside a firmware graph, not at the DAI/BE or host/FE levels, and such intermediate nodes are usually not handled by soc-pcm. We really need more than the notion of FE and BE, a two-layer solution is no longer sufficient.
The other thing that looks weird is the dependency on both sink and source sharing a common state. For a noise reduction there are cases where you'd want the mic input to be stored in a history buffer so that the noise parameters can be estimated as soon as the actual capture starts.
Which PCMs are needed for given conditional PCM to be spawned is determinated by the driver when registering the condpcm.
Presumably such links should be described by a topology file? It would be odd for a driver to have to guess when to connect processing elements.
The functionality was initially proposed for the avs-driver [1] and, depending on feedback and review may either go back into avs -or- become a ASoC-core feature. Implementation present here is an example of how such functionality could look and work on the ASoC side. Compared to what was provided initially, the patch carries simplified version of the feature: no priority/overriding for already running conditional PCMs. Whatever is spawned is treated as a non-conflicting entity.
Assumptions and design decisions:
- existence and outcome of condpcm operations is entirely optional and shall not impact the runtime flow of PCMs that spawned given condpcm, e.g.: fail in cpcm->hw_params() shall not impact fe->hw_params() or be->hw_params() negatively. Think of it as of debugfs. Useful? Yes. Required for system to operate? No.
that's debatable, if the AEC setup isn't successful then is the functionality implemented correctly? My take is no, don't fail silently if the AEC doesn't work.
If this functionality is listed as a product requirement then it cannot be treated as a debugfs optional thing.
Exhibit A for this is the countless cases where validation reported a problem with a path remaining active or conversely not being setup, or a voice quality issue. Those are not optional...
a condpcm is a runtime entity that's audio format independent - since certain FE/BEs are its dependencies already, that's no need to do format ruling twice. Driver may still do custom checks thanks to ->match() operation.
a condpcm allows for additional processing of data that flows from data-source - a substream instance acting as data provider - to sink - a substream acting as data consumer. At the same time, regardless of substream->stream, given substream may act as data source for one condpcm and data sink for another, simultaneously.
while condpcm's behaviour mimics standard PCM one, there is no ->open() and ->close() - FE/BEs are treated as operational starting with successful ->hw_params(), when hw_ruling is done and hardware is configured.
cpcm->prepare() gets called only when both data source and sink are prepared
cpcm->trigger(START) gets called only when both data source and sink are running
cpcm->trigger(STOP) gets called when either data source or sink is stopped
Simplified state machine:
|
register_condpcm() | v +--+-------------+ | DISCONNECTED |<-+ +--+-------------+ | | | condpcm_hw_params() | | v | +--+-------------+ | | SETUP | | condpcm_hw_free() +--+-------------+ | | | condpcm_prepare() | | v | +--+----+--------+ | | PREPARED |--+ +--+----------+--+ | ^ condpcm_start() | | condpcm_stop() v | +--+----------+--+ | RUNNING | +----------------+
What's missing? I've not covered the locking part yet. While some operations are covered by default thanks to snd_soc_dpcm_mutex(), it is insufficient. If feature goes back to the avs-driver, then I'm set due to path->mutex.
The locking is one of the reasons I'm leaning towards leaving the condpcm within the avs-driver. For soc_condpcm_find_match() to be precise and do no harm, a lock must prepend the list_for_each_entry(). Entries (substreams) of that list may be part of number of different components and the search may negatively impact runtime flow of substreams that do not care about condpcms at all.
Has this been tested?
Unit-like only. Typical case below with avs_condpcm_ops representing bunch of stubs with printfs.
static struct snd_soc_condpcm_pred pred1 = { .card_name = "ssp0-loopback", .link_name = "SSP0-Codec", /* BE */ .direction = SNDRV_PCM_STREAM_PLAYBACK, };
static struct snd_soc_condpcm_pred pred2 = { .card_name = "hdaudioB0D2", .link_name = "HDMI1", /* FE */ .direction = SNDRV_PCM_STREAM_PLAYBACK, };
It's not intuitive to follow what HDMI and SSP might have to do with each other, nor why one is a BE and one is an FE?
If I follow the code below, the SSP loopback is a source feeds into an HDMI sink, and SSP is a BE and HDMI an FE? Confusing example...
static void avs_condpcms_register(struct avs_dev *adev) { (...) snd_soc_register_condpcm(&pred1, &pred2, &avs_condpcm_ops, adev); }
Signed-off-by: Cezary Rojewski cezary.rojewski@intel.com
include/sound/pcm.h | 1 + include/sound/soc.h | 65 ++++++++ sound/core/pcm.c | 1 + sound/soc/Makefile | 2 +- sound/soc/soc-condpcm.c | 348 ++++++++++++++++++++++++++++++++++++++++ sound/soc/soc-condpcm.h | 17 ++ sound/soc/soc-core.c | 2 + sound/soc/soc-pcm.c | 11 ++ 8 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 sound/soc/soc-condpcm.c create mode 100644 sound/soc/soc-condpcm.h
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;
It wouldn't hurt to describe what 'candidate' might mean here?
/* -- 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;
Please tell me the runtimes and links are in the same card... If not, there's all kinds of power management and probe/remove issues...
- const char *link_name;
dai 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.
predicate is a verb and a noun, not clear what you are trying to document.
- @source: substreaming acting as a data source, assigned at runtime.
- @sink: substreaming acting as a data sink, assigned at runtime.
- @state: current runtime state.
presumably this state is already defined that the state of sink/source?
- @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;
sprintf(substream->name, "subdevice #%i", idx); substream->buffer_bytes_max = UINT_MAX; if (prev == NULL)INIT_LIST_HEAD(&substream->cpcm_candidate_node);
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);
how are the 'params' defined?
I read above
" a condpcm is a runtime entity that's audio format independent - since certain FE/BEs are its dependencies already, that's no need to do format ruling twice. "
That doesn't tell us how this 'params' is determined. This is important for cases where the speaker output is e.g. 2ch 48kHz and the mic input is 4ch 96kHz. If this condpcm is not managed by any usersapce action, then what is the logic for selecting the settings in 'params'?
- 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;
+}
There's also the well-known problem that hw_params can be called multiple times. Here this wouldn't work with the same source/sink added multiple times in a list.
+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;
+}
you probably need to look at the xruns and resume cases, where prepare() is used for vastly different purposes.
+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);
+}
Are loops supported? Is the order between capture and playback intentional? Is the notion of playback/capture even relevant when trying to add loopbacks?
Lots of questions...
On 2024-08-21 2:43 PM, Pierre-Louis Bossart wrote:
On 8/21/24 12:18, Cezary Rojewski wrote:
Conditional PCM (condpcm) helps facilitate modern audio usecases such as Echo Cancellations and Noise Reduction. These are not invoked by the means of userspace application opening an endpoint (FrontEnd) but are a "side effect" of selected PCMs running simultaneously e.g.: if both Speaker (source) and Microphone Array (sink) are running, reference data from the Speaker and take it into account when processing capture for better voice command detection ratio.
After reading the review, it's important for me to highlight that the quality of the response is high and required that much effort to write it. Thank you.
The point about dependencies between capture/playback usages is certainly valid, and we've faced it multiple times for SOF - and even before in the mobile phone days. I am not convinced however that the graph management suggested here solves the well-known DPCM routing problems? See notes in no specific order below.
While at it, do we (Mark perhaps?) have some kind of a list with major problems troubling ASoC? I keep seeing "DPCM is problematic" on the mailing-list. If DPCM is indeed in such bad state, perhaps we should address this sooner rather than later.
I am not following what the 'source' and 'sink' concepts refer to in this context. It looks like you are referring to regular PCM devices, i.e. Front Ends in soc-pcm parlance but examples and code make references to Back Ends.
There are also complicated cases where the amplifiers can provide an echo reference for AEC and I/V sensing for speaker protection. You would want to capture both even if there's no capture happening at the userspace level. This is a well-know DPCM routing issue where we have to rely on a Front-End being opened and some tags in UCM to deal with loose coupling.
It would help if you added precisions on your assumptions of where the processing takes place. In some cases Echo cancellation is handled in userspace, others in SOC firmware and others externally in a codec.
The notion of source/sink is also problematic when the same BE provides two sources of information that will be split, again same problem with amplifier feedback being used for two separate functions. What happens if you have multiple sinks for one source?
Same for the cases where the mic input is split multiple ways with different processing added on different PCM capture devices, e.g. for WebRTC there's an ask for a raw input, an AEC-processed input and AEC+NS-processed input. That's typically implemented with two splitters, the echo reference would be used by an intermediate node inside a firmware graph, not at the DAI/BE or host/FE levels, and such intermediate nodes are usually not handled by soc-pcm. We really need more than the notion of FE and BE, a two-layer solution is no longer sufficient.
The other thing that looks weird is the dependency on both sink and source sharing a common state. For a noise reduction there are cases where you'd want the mic input to be stored in a history buffer so that the noise parameters can be estimated as soon as the actual capture starts.
I'm used to environment where most of the processing is done by the SOC firmware so that would be one of the design philosophies.
The reason I've opted out from using "FE/BE" is to avoid naming confusion. FE/BEs are paired with dai_links and explicitly state the value of ->no_pcm flag. Condpcm does not care about that flag at all and given snd_soc_pcm_runtime (rtd) instance can be utilized simultaneously as data provider _and_ data consumer. The existing approach allows for source -> sink models: BE -> BE and FE -> FE both, I believe this helps in amplifier case.
You've shared many scenarios, I do not think we can cover all of them here and while I could agree that current FE/BE (DPCM?) design did not age well, we're entering "rewrite how-to-pcm-in-linux" area. If general opinion is: it's too much, we have to rewrite for the framework to scale into the next 20 years of audio in Linux
then my thoughts regarding current review are: if the avs-driver needs sideband interface, so be it, but do it locally rather than polluting entire framework. Switch to the framework-solution once its rewritten.
Which PCMs are needed for given conditional PCM to be spawned is determinated by the driver when registering the condpcm.
Presumably such links should be described by a topology file? It would be odd for a driver to have to guess when to connect processing elements.
Indeed, topology can help here. Of course if a driver is utilizing static connections, one could register condpcm using pre-defined values.
The functionality was initially proposed for the avs-driver [1] and, depending on feedback and review may either go back into avs -or- become a ASoC-core feature. Implementation present here is an example of how such functionality could look and work on the ASoC side. Compared to what was provided initially, the patch carries simplified version of the feature: no priority/overriding for already running conditional PCMs. Whatever is spawned is treated as a non-conflicting entity.
Assumptions and design decisions:
- existence and outcome of condpcm operations is entirely optional and shall not impact the runtime flow of PCMs that spawned given condpcm, e.g.: fail in cpcm->hw_params() shall not impact fe->hw_params() or be->hw_params() negatively. Think of it as of debugfs. Useful? Yes. Required for system to operate? No.
that's debatable, if the AEC setup isn't successful then is the functionality implemented correctly? My take is no, don't fail silently if the AEC doesn't work.
If this functionality is listed as a product requirement then it cannot be treated as a debugfs optional thing.
Exhibit A for this is the countless cases where validation reported a problem with a path remaining active or conversely not being setup, or a voice quality issue. Those are not optional...
Well, as you mentioned, that's debatable. Perhaps an opt-in flag is needed here - I'd like not to put all the users into same basket. Some users may not be happy with AEC failing but would like their speakers/mics to keep working nonetheless. Basic functionality better than no functionality. Either flag or 'return 0' if you do not care methodology.
a condpcm is a runtime entity that's audio format independent - since certain FE/BEs are its dependencies already, that's no need to do format ruling twice. Driver may still do custom checks thanks to ->match() operation.
a condpcm allows for additional processing of data that flows from data-source - a substream instance acting as data provider - to sink - a substream acting as data consumer. At the same time, regardless of substream->stream, given substream may act as data source for one condpcm and data sink for another, simultaneously.
while condpcm's behaviour mimics standard PCM one, there is no ->open() and ->close() - FE/BEs are treated as operational starting with successful ->hw_params(), when hw_ruling is done and hardware is configured.
cpcm->prepare() gets called only when both data source and sink are prepared
cpcm->trigger(START) gets called only when both data source and sink are running
cpcm->trigger(STOP) gets called when either data source or sink is stopped
Simplified state machine:
|
register_condpcm() | v +--+-------------+ | DISCONNECTED |<-+ +--+-------------+ | | | condpcm_hw_params() | | v | +--+-------------+ | | SETUP | | condpcm_hw_free() +--+-------------+ | | | condpcm_prepare() | | v | +--+----+--------+ | | PREPARED |--+ +--+----------+--+ | ^ condpcm_start() | | condpcm_stop() v | +--+----------+--+ | RUNNING | +----------------+
What's missing? I've not covered the locking part yet. While some operations are covered by default thanks to snd_soc_dpcm_mutex(), it is insufficient. If feature goes back to the avs-driver, then I'm set due to path->mutex.
The locking is one of the reasons I'm leaning towards leaving the condpcm within the avs-driver. For soc_condpcm_find_match() to be precise and do no harm, a lock must prepend the list_for_each_entry(). Entries (substreams) of that list may be part of number of different components and the search may negatively impact runtime flow of substreams that do not care about condpcms at all.
Has this been tested?
Unit-like only. Typical case below with avs_condpcm_ops representing bunch of stubs with printfs.
static struct snd_soc_condpcm_pred pred1 = { .card_name = "ssp0-loopback", .link_name = "SSP0-Codec", /* BE */ .direction = SNDRV_PCM_STREAM_PLAYBACK, };
static struct snd_soc_condpcm_pred pred2 = { .card_name = "hdaudioB0D2", .link_name = "HDMI1", /* FE */ .direction = SNDRV_PCM_STREAM_PLAYBACK, };
It's not intuitive to follow what HDMI and SSP might have to do with each other, nor why one is a BE and one is an FE?
If I follow the code below, the SSP loopback is a source feeds into an HDMI sink, and SSP is a BE and HDMI an FE? Confusing example...
The intention is to show that condpcm does not care what exactly source/sink represent. It just connects rtds which may lie on completely different sound cards. This very example will treat BE dai_link named "SSP0-Codec" as data source for FE dai_link named "HDMI1" which consumes the data (sink).
static void avs_condpcms_register(struct avs_dev *adev) { (...) snd_soc_register_condpcm(&pred1, &pred2, &avs_condpcm_ops, adev); }
Signed-off-by: Cezary Rojewski cezary.rojewski@intel.com
include/sound/pcm.h | 1 + include/sound/soc.h | 65 ++++++++ sound/core/pcm.c | 1 + sound/soc/Makefile | 2 +- sound/soc/soc-condpcm.c | 348 ++++++++++++++++++++++++++++++++++++++++ sound/soc/soc-condpcm.h | 17 ++ sound/soc/soc-core.c | 2 + sound/soc/soc-pcm.c | 11 ++ 8 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 sound/soc/soc-condpcm.c create mode 100644 sound/soc/soc-condpcm.h
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;
It wouldn't hurt to describe what 'candidate' might mean here?
Ack.
...
+/* 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;
Please tell me the runtimes and links are in the same card... If not, there's all kinds of power management and probe/remove issues...
Well, this have been kind of mentioned by me in "What's missing?". I've focused more on the locking part though. However, register() and unregister() functions are explicit, the condpcm-owning driver should be responsible for handling the problematic pieces highlighted above.
- const char *link_name;
dai link name?
Meh, .card_name and .link_name have the exact same length.
- int direction;
+};
+/**
- struct snd_soc_condpcm - Conditional PCM descriptor.
- @ops: custom PCM operations.
- @preds: predicates for identifying source and sink for given conditional PCM.
predicate is a verb and a noun, not clear what you are trying to document.
In this context 'predicate' is a descriptor, selected set of features to test in order to answer the question: Does this substream match given condpcm?
- @source: substreaming acting as a data source, assigned at runtime.
- @sink: substreaming acting as a data sink, assigned at runtime.
- @state: current runtime state.
presumably this state is already defined that the state of sink/source?
The state keeps things sane - avoid duplicate calls etc. It does not represent state of sink/source directly, one has to acccess dpcm do obtain that information.
...
+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);
how are the 'params' defined?
I read above
" a condpcm is a runtime entity that's audio format independent - since certain FE/BEs are its dependencies already, that's no need to do format ruling twice. "
That doesn't tell us how this 'params' is determined. This is important for cases where the speaker output is e.g. 2ch 48kHz and the mic input is 4ch 96kHz. If this condpcm is not managed by any usersapce action, then what is the logic for selecting the settings in 'params'?
soc-condpcm.c tries to be cohesive with the rest of soc-pcm.c code. I do not see a reason to create _new_ naming scheme, so here I just mimic ->hw_params() behaviour. The callee may choose to ignore 'params' or may take it into account. In regard to your question, the callee may check rtd->dpcm[stream].hw_params when servicing ->match(cpcm, source, sink) to do necessary examination.
- 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;
+}
There's also the well-known problem that hw_params can be called multiple times. Here this wouldn't work with the same source/sink added multiple times in a list.
soc_condpcm_walk() invokes soc_condpcm_hw_params() only if cpcm->state equals DISCONNECTED. If I misunderstood, please elaborate.
+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;
+}
you probably need to look at the xruns and resume cases, where prepare() is used for vastly different purposes.
My initial idea was to cut prepare() step entirely. Perhaps that's the way to go.
...
+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);
+}
Are loops supported? Is the order between capture and playback intentional? Is the notion of playback/capture even relevant when trying to add loopbacks?
Lots of questions...
The order selected here is more of a habbit of mine, not a must-be. Loopback scenario implies a real capture endpoint which is sourced from certain playback stream. If that's the ask, yes, it's one of the usecases.
On Thu, Aug 22, 2024 at 03:57:22PM +0200, Cezary Rojewski wrote:
On 2024-08-21 2:43 PM, Pierre-Louis Bossart wrote:
The point about dependencies between capture/playback usages is certainly valid, and we've faced it multiple times for SOF - and even before in the mobile phone days. I am not convinced however that the graph management suggested here solves the well-known DPCM routing problems? See notes in no specific order below.
While at it, do we (Mark perhaps?) have some kind of a list with major problems troubling ASoC? I keep seeing "DPCM is problematic" on the mailing-list. If DPCM is indeed in such bad state, perhaps we should address this sooner rather than later.
Honestly I think DPCM is the only major problem we've got. The problem is that addressing it is very non-trivial, we've had a rough design for what we want to do for about a decade (look for some of Lars-Peter's ELC presentations for some summaries) but what we're missing is someone with the required combination of time and enthusiasm to actually implement it. A lot of the refactorings that Morimoto-san has been doing which move everything to be a component rather than distinct CODEC, CPU or platform types has been preparatory to the main refactoring.
On Thu, Aug 22, 2024 at 03:57:22PM +0200, Cezary Rojewski wrote:
You've shared many scenarios, I do not think we can cover all of them here and while I could agree that current FE/BE (DPCM?) design did not age well, we're entering "rewrite how-to-pcm-in-linux" area. If general opinion is: it's too much, we have to rewrite for the framework to scale into the next 20 years of audio in Linux
then my thoughts regarding current review are: if the avs-driver needs sideband interface, so be it, but do it locally rather than polluting entire framework. Switch to the framework-solution once its rewritten.
On this bit as I mentioned in the prior reply there's been ideas for redesigning how we tackle digital audio which I think there's general agreement would be the best way forwards. DPCM is very fragile and creaking at the seams, it can't really represent scenarios like the sideband case you've mentioned well. OTOH a redesign is a very big lift and there's never really a point where it seems constructive to actually block things on it so long as everyone involved is OK with what's going on.
The upshot is that while I'd be *really* happy to see investment in the framework side of things I probably wouldn't block a driver specific or DPCM solution simply on the grounds that it's messy. DPCM would need buy in from other people using DPCM of course, and hopefully at some point someone with one of these issues will find that the cost of maintaining a bodge is going to be enough to push them to do the work (or someone will have free time to just work on the issue).
On 2024-09-10 10:14 PM, Mark Brown wrote:
On Thu, Aug 22, 2024 at 03:57:22PM +0200, Cezary Rojewski wrote:
You've shared many scenarios, I do not think we can cover all of them here and while I could agree that current FE/BE (DPCM?) design did not age well, we're entering "rewrite how-to-pcm-in-linux" area. If general opinion is: it's too much, we have to rewrite for the framework to scale into the next 20 years of audio in Linux
then my thoughts regarding current review are: if the avs-driver needs sideband interface, so be it, but do it locally rather than polluting entire framework. Switch to the framework-solution once its rewritten.
On this bit as I mentioned in the prior reply there's been ideas for redesigning how we tackle digital audio which I think there's general agreement would be the best way forwards. DPCM is very fragile and creaking at the seams, it can't really represent scenarios like the sideband case you've mentioned well. OTOH a redesign is a very big lift and there's never really a point where it seems constructive to actually block things on it so long as everyone involved is OK with what's going on.
The upshot is that while I'd be *really* happy to see investment in the framework side of things I probably wouldn't block a driver specific or DPCM solution simply on the grounds that it's messy. DPCM would need buy in from other people using DPCM of course, and hopefully at some point someone with one of these issues will find that the cost of maintaining a bodge is going to be enough to push them to do the work (or someone will have free time to just work on the issue).
From my experience when it comes to redesigning/rewriting entire project, the "upgrade an already running train" strategy does not work, so I'd not recommend that.
Instead, I'd suggest to have a second, separate DPCM implementation present next to the existing one and have users opt in during a so-called deprecation period of the existing one. Once certain amount of time passes, switch all of them. Clean slide also makes it easier for a developer to be creative.
Do you view the second option as viable?
On 9/13/24 12:59, Cezary Rojewski wrote:
On 2024-09-10 10:14 PM, Mark Brown wrote:
On Thu, Aug 22, 2024 at 03:57:22PM +0200, Cezary Rojewski wrote:
You've shared many scenarios, I do not think we can cover all of them here and while I could agree that current FE/BE (DPCM?) design did not age well, we're entering "rewrite how-to-pcm-in-linux" area. If general opinion is: it's too much, we have to rewrite for the framework to scale into the next 20 years of audio in Linux
then my thoughts regarding current review are: if the avs-driver needs sideband interface, so be it, but do it locally rather than polluting entire framework. Switch to the framework-solution once its rewritten.
On this bit as I mentioned in the prior reply there's been ideas for redesigning how we tackle digital audio which I think there's general agreement would be the best way forwards. DPCM is very fragile and creaking at the seams, it can't really represent scenarios like the sideband case you've mentioned well. OTOH a redesign is a very big lift and there's never really a point where it seems constructive to actually block things on it so long as everyone involved is OK with what's going on.
The upshot is that while I'd be *really* happy to see investment in the framework side of things I probably wouldn't block a driver specific or DPCM solution simply on the grounds that it's messy. DPCM would need buy in from other people using DPCM of course, and hopefully at some point someone with one of these issues will find that the cost of maintaining a bodge is going to be enough to push them to do the work (or someone will have free time to just work on the issue).
From my experience when it comes to redesigning/rewriting entire project, the "upgrade an already running train" strategy does not work, so I'd not recommend that.
Instead, I'd suggest to have a second, separate DPCM implementation present next to the existing one and have users opt in during a so- called deprecation period of the existing one. Once certain amount of time passes, switch all of them. Clean slide also makes it easier for a developer to be creative.
Do you view the second option as viable?
Classic problem without a good solution. In practice new features or corrections get added to the 'old' framework and the new one is not used by anyone just yet so it keeps running behind in terms of features/maturity. And due to limited validation resources or limited access to a wide variety of hardware, no one is quite ready to enable the new framework on 'old' platforms because that would break quite a few devices.
The other problem is that the 'switch' would mean a compatible solution, but the problem is to get rid of the very notion of front- and back-ends. Existing users of DPCM have undocumented hard-coded dependencies on the order in which callbacks are handled, it's not easy at all to change the routing engine.
On Fri, Sep 13, 2024 at 01:53:41PM +0200, Pierre-Louis Bossart wrote:
On 9/13/24 12:59, Cezary Rojewski wrote:
Instead, I'd suggest to have a second, separate DPCM implementation present next to the existing one and have users opt in during a so- called deprecation period of the existing one. Once certain amount of time passes, switch all of them. Clean slide also makes it easier for a developer to be creative.
Do you view the second option as viable?
Classic problem without a good solution. In practice new features or corrections get added to the 'old' framework and the new one is not used by anyone just yet so it keeps running behind in terms of features/maturity. And due to limited validation resources or limited access to a wide variety of hardware, no one is quite ready to enable the new framework on 'old' platforms because that would break quite a few devices.
Yeah, there's no idea way of doing things here. I do think that keeping things going in parallel is probably the most viable way of doing things. You'd need at least one lead platform using the new stuff, and probably each platform would have a flag day to transition.
I'm a little more optimistic than Pierre about making progress, hopefully things would be sufficiently nicer that once there's something to build on it'd start to get more attractive to do the incremental investments needed in the core to enable more platforms, and reduced maintainance costs get more appealing. This approach has worked for other things (regmap springs to mind, and if you look at some of the way API transitions are done in the memory management and arch code there's a bunch of this going on) though it's rarely fast.
The other problem is that the 'switch' would mean a compatible solution, but the problem is to get rid of the very notion of front- and back-ends. Existing users of DPCM have undocumented hard-coded dependencies on the order in which callbacks are handled, it's not easy at all to change the routing engine.
Each platform is going to need to be pretty careful with the conversion and validating that things work. There are some simpler ones which will probably have an easier time of it, though. There's some of them like Kirkwood that are just at the lower end of needing DPCM support at all, and others that didn't upstream so much stuff yet so there's less knowledge embedded in the upstream code.
Hi
From my experience when it comes to redesigning/rewriting entire project, the "upgrade an already running train" strategy does not work, so I'd not recommend that.
Instead, I'd suggest to have a second, separate DPCM implementation present next to the existing one and have users opt in during a so- called deprecation period of the existing one. Once certain amount of time passes, switch all of them. Clean slide also makes it easier for a developer to be creative.
Do you view the second option as viable?
Classic problem without a good solution. In practice new features or corrections get added to the 'old' framework and the new one is not used by anyone just yet so it keeps running behind in terms of features/maturity. And due to limited validation resources or limited access to a wide variety of hardware, no one is quite ready to enable the new framework on 'old' platforms because that would break quite a few devices.
The other problem is that the 'switch' would mean a compatible solution, but the problem is to get rid of the very notion of front- and back-ends. Existing users of DPCM have undocumented hard-coded dependencies on the order in which callbacks are handled, it's not easy at all to change the routing engine.
I can agree that upgrading existing framework is not good idea.
My Idea is push exising "old" framework as v1, and create new framework as v2. We can use both version in the same time. And we can add more new version in the future (if needed).
I think we can use "${LINUX}/sound/soc/generic/test-component.c" to test v2 framework which is dummy CPU/Codec. Everyone can use and test it without HW. We already using it as Audio-Graph-Card2 sample. (${LINUX}/sound/soc/generic/audio-graph-card2-custom-sample.dtsi)
Thank you for your help !!
Best regards --- Kuninori Morimoto
participants (4)
-
Cezary Rojewski
-
Kuninori Morimoto
-
Mark Brown
-
Pierre-Louis Bossart