Re: [RFC PATCH v2 5/5] ASoC: apple: Add macaudio machine driver

From: Pierre-Louis Bossart
Date: Mon Jun 06 2022 - 16:02:38 EST



> + * Virtual FE/BE Playback Topology
> + * -------------------------------
> + *
> + * The platform driver has independent frontend and backend DAIs with the
> + * option of routing backends to any of the frontends. The platform
> + * driver configures the routing based on DPCM couplings in ASoC runtime
> + * structures, which in turn is determined from DAPM paths by ASoC. But the
> + * platform driver doesn't supply relevant DAPM paths and leaves that up for
> + * the machine driver to fill in. The filled-in virtual topology can be
> + * anything as long as a particular backend isn't connected to more than one
> + * frontend at any given time. (The limitation is due to the unsupported case
> + * of reparenting of live BEs.)
> + *
> + * The DAPM routing that this machine-level driver makes up has two use-cases
> + * in mind:
> + *
> + * - Using a single PCM for playback such that it conditionally sinks to either
> + * speakers or headphones based on the plug-in state of the headphones jack.
> + * All the while making the switch transparent to userspace. This has the
> + * drawback of requiring a sample stream suited for both speakers and
> + * headphones, which is hard to come by on machines where tailored DSP for
> + * speakers in userspace is desirable or required.
> + *
> + * - Driving the headphones and speakers from distinct PCMs, having userspace
> + * bridge the difference and apply different signal processing to the two.
> + *
> + * In the end the topology supplied by this driver looks like this:
> + *
> + * PCMs (frontends) I2S Port Groups (backends)
> + * ──────────────── ──────────────────────────
> + *
> + * ┌──────────┐ ┌───────────────► ┌─────┐ ┌──────────┐
> + * │ Primary ├───────┤ │ Mux │ ──► │ Speakers │
> + * └──────────┘ │ ┌──────────► └─────┘ └──────────┘
> + * ┌─── │ ───┘ ▲
> + * ┌──────────┐ │ │ │
> + * │Secondary ├──┘ │ ┌────────────┴┐
> + * └──────────┘ ├────►│Plug-in Demux│
> + * │ └────────────┬┘
> + * │ │
> + * │ ▼
> + * │ ┌─────┐ ┌──────────┐
> + * └───────────────► │ Mux │ ──► │Headphones│
> + * └─────┘ └──────────┘
> + */

In Patch2, the 'clusters' are described as front-ends, with I2S as
back-ends. Here the PCMs are described as front-ends, but there's no
mention of clusters. Either one of the two descriptions is outdated, or
there's something missing to help reconcile the two pieces of information?


> +static int macaudio_copy_link(struct device *dev, struct snd_soc_dai_link *target,
> + struct snd_soc_dai_link *source)
> +{
> + memcpy(target, source, sizeof(struct snd_soc_dai_link));
> +
> + target->cpus = devm_kcalloc(dev, target->num_cpus,
> + sizeof(*target->cpus), GFP_KERNEL);
> + target->codecs = devm_kcalloc(dev, target->num_codecs,
> + sizeof(*target->codecs), GFP_KERNEL);
> + target->platforms = devm_kcalloc(dev, target->num_platforms,
> + sizeof(*target->platforms), GFP_KERNEL);
> +
> + if (!target->cpus || !target->codecs || !target->platforms)
> + return -ENOMEM;
> +
> + memcpy(target->cpus, source->cpus, sizeof(*target->cpus) * target->num_cpus);
> + memcpy(target->codecs, source->codecs, sizeof(*target->codecs) * target->num_codecs);
> + memcpy(target->platforms, source->platforms, sizeof(*target->platforms) * target->num_platforms);


use devm_kmemdup?

> +
> + return 0;
> +}

> +static int macaudio_get_runtime_mclk_fs(struct snd_pcm_substream *substream)
> +{
> + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card);
> + struct snd_soc_dpcm *dpcm;
> +
> + /*
> + * If this is a FE, look it up in link_props directly.
> + * If this is a BE, look it up in the respective FE.
> + */
> + if (!rtd->dai_link->no_pcm)
> + return ma->link_props[rtd->dai_link->id].mclk_fs;
> +
> + for_each_dpcm_fe(rtd, substream->stream, dpcm) {
> + int fe_id = dpcm->fe->dai_link->id;
> +
> + return ma->link_props[fe_id].mclk_fs;
> + }

I am not sure what the concept of mclk would mean for a front-end? This
is typically very I2S-specific, i.e. a back-end property, no?

> +
> + return 0;
> +}
> +
> +static int macaudio_dpcm_hw_params(struct snd_pcm_substream *substream,
> + struct snd_pcm_hw_params *params)
> +{
> + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
> + struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
> + int mclk_fs = macaudio_get_runtime_mclk_fs(substream);
> + int i;
> +
> + if (mclk_fs) {
> + struct snd_soc_dai *dai;
> + int mclk = params_rate(params) * mclk_fs;
> +
> + for_each_rtd_codec_dais(rtd, i, dai)
> + snd_soc_dai_set_sysclk(dai, 0, mclk, SND_SOC_CLOCK_IN);
> +
> + snd_soc_dai_set_sysclk(cpu_dai, 0, mclk, SND_SOC_CLOCK_OUT);
> + }
> +
> + return 0;
> +}
> +
> +static void macaudio_dpcm_shutdown(struct snd_pcm_substream *substream)
> +{
> + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
> + struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
> + struct snd_soc_dai *dai;
> + int mclk_fs = macaudio_get_runtime_mclk_fs(substream);
> + int i;
> +
> + if (mclk_fs) {
> + for_each_rtd_codec_dais(rtd, i, dai)
> + snd_soc_dai_set_sysclk(dai, 0, 0, SND_SOC_CLOCK_IN);
> +
> + snd_soc_dai_set_sysclk(cpu_dai, 0, 0, SND_SOC_CLOCK_OUT);
> + }
> +}
> +
> +static const struct snd_soc_ops macaudio_fe_ops = {
> + .shutdown = macaudio_dpcm_shutdown,
> + .hw_params = macaudio_dpcm_hw_params,
> +};
> +
> +static const struct snd_soc_ops macaudio_be_ops = {
> + .shutdown = macaudio_dpcm_shutdown,
> + .hw_params = macaudio_dpcm_hw_params,
> +};
> +
> +static int macaudio_be_assign_tdm(struct snd_soc_pcm_runtime *rtd)
> +{
> + struct snd_soc_card *card = rtd->card;
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
> + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
> + struct snd_soc_dai *dai;
> + unsigned int mask;
> + int nslots, ret, i;
> +
> + if (!props->tdm_mask)
> + return 0;
> +
> + mask = props->tdm_mask;
> + nslots = __fls(mask) + 1;
> +
> + if (rtd->num_codecs == 1) {
> + ret = snd_soc_dai_set_tdm_slot(asoc_rtd_to_codec(rtd, 0), mask,
> + 0, nslots, MACAUDIO_SLOTWIDTH);
> +
> + /*
> + * Headphones get a pass on -EOPNOTSUPP (see the comment
> + * around mclk_fs value for primary FE).
> + */
> + if (ret == -EOPNOTSUPP && props->is_headphones)
> + return 0;
> +
> + return ret;
> + }
> +
> + for_each_rtd_codec_dais(rtd, i, dai) {
> + int slot = __ffs(mask);
> +
> + mask &= ~(1 << slot);
> + ret = snd_soc_dai_set_tdm_slot(dai, 1 << slot, 0, nslots,
> + MACAUDIO_SLOTWIDTH);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static int macaudio_be_init(struct snd_soc_pcm_runtime *rtd)
> +{
> + struct snd_soc_card *card = rtd->card;
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
> + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
> + struct snd_soc_dai *dai;
> + int i, ret;
> +
> + ret = macaudio_be_assign_tdm(rtd);
> + if (ret < 0)
> + return ret;
> +
> + if (props->is_headphones) {
> + for_each_rtd_codec_dais(rtd, i, dai)
> + snd_soc_component_set_jack(dai->component, &ma->jack, NULL);
> + }

this is weird, set_jack() is invoked by the ASoC core. You shouldn't
need to do this?

> +
> + return 0;
> +}
> +
> +static void macaudio_be_exit(struct snd_soc_pcm_runtime *rtd)
> +{
> + struct snd_soc_card *card = rtd->card;
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
> + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
> + struct snd_soc_dai *dai;
> + int i;
> +
> + if (props->is_headphones) {
> + for_each_rtd_codec_dais(rtd, i, dai)
> + snd_soc_component_set_jack(dai->component, NULL, NULL);
> + }

same, why is this needed?

> +}
> +
> +static int macaudio_fe_init(struct snd_soc_pcm_runtime *rtd)
> +{
> + struct snd_soc_card *card = rtd->card;
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
> + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
> + int nslots = props->mclk_fs / MACAUDIO_SLOTWIDTH;
> +
> + return snd_soc_dai_set_tdm_slot(asoc_rtd_to_cpu(rtd, 0), (1 << nslots) - 1,
> + (1 << nslots) - 1, nslots, MACAUDIO_SLOTWIDTH);
> +}
> +
> +
> +static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
> + void *data);
> +
> +static struct notifier_block macaudio_jack_nb = {
> + .notifier_call = macaudio_jack_event,
> +};

why is this needed? we have already many ways of dealing with the jack
events (dare I say too many ways?).
> +
> +static int macaudio_probe(struct snd_soc_card *card)
> +{
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
> + int ret;
> +
> + ma->pin.pin = "Headphones";
> + ma->pin.mask = SND_JACK_HEADSET | SND_JACK_HEADPHONE;
> + ret = snd_soc_card_jack_new(card, ma->pin.pin,
> + SND_JACK_HEADSET |
> + SND_JACK_HEADPHONE |
> + SND_JACK_BTN_0 | SND_JACK_BTN_1 |
> + SND_JACK_BTN_2 | SND_JACK_BTN_3,
> + &ma->jack, &ma->pin, 1);
> +
> + if (ret < 0) {
> + dev_err(card->dev, "jack creation failed: %d\n", ret);
> + return ret;
> + }
> +
> + snd_soc_jack_notifier_register(&ma->jack, &macaudio_jack_nb);
> +
> + return ret;
> +}
> +
> +static int macaudio_add_backend_dai_route(struct snd_soc_card *card, struct snd_soc_dai *dai,
> + bool is_speakers)
> +{
> + struct snd_soc_dapm_route routes[2];
> + int nroutes;
> + int ret;

newline?

> + memset(routes, 0, sizeof(routes));
> +
> + dev_dbg(card->dev, "adding routes for '%s'\n", dai->name);
> +
> + if (is_speakers)
> + routes[0].source = "Speakers Playback";
> + else
> + routes[0].source = "Headphones Playback";
> + routes[0].sink = dai->playback_widget->name;
> + nroutes = 1;
> +
> + if (!is_speakers) {
> + routes[1].source = dai->capture_widget->name;
> + routes[1].sink = "Headphones Capture";
> + nroutes = 2;
> + }
> +
> + ret = snd_soc_dapm_add_routes(&card->dapm, routes, nroutes);
> + if (ret)
> + dev_err(card->dev, "failed adding dynamic DAPM routes for %s\n",
> + dai->name);
> + return ret;
> +}
> +
> +static bool macaudio_match_kctl_name(const char *pattern, const char *name)
> +{
> + if (pattern[0] == '*') {
> + int namelen, patternlen;
> +
> + pattern++;
> + if (pattern[0] == ' ')
> + pattern++;
> +
> + namelen = strlen(name);
> + patternlen = strlen(pattern);
> +
> + if (namelen > patternlen)
> + name += (namelen - patternlen);
> + }
> +
> + return !strcmp(name, pattern);
> +}
> +
> +static int macaudio_limit_volume(struct snd_soc_card *card,
> + const char *pattern, int max)
> +{
> + struct snd_kcontrol *kctl;
> + struct soc_mixer_control *mc;
> + int found = 0;
> +
> + list_for_each_entry(kctl, &card->snd_card->controls, list) {
> + if (!macaudio_match_kctl_name(pattern, kctl->id.name))
> + continue;
> +
> + found++;
> + dev_dbg(card->dev, "limiting volume on '%s'\n", kctl->id.name);
> +
> + /*
> + * TODO: This doesn't decrease the volume if it's already
> + * above the limit!
> + */
> + mc = (struct soc_mixer_control *)kctl->private_value;
> + if (max <= mc->max)
> + mc->platform_max = max;
> +
> + }
> +
> + return found;
> +}
> +
> +static int macaudio_late_probe(struct snd_soc_card *card)
> +{
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
> + struct snd_soc_pcm_runtime *rtd;
> + struct snd_soc_dai *dai;
> + int ret, i;
> +
> + /* Add the dynamic DAPM routes */
> + for_each_card_rtds(card, rtd) {
> + struct macaudio_link_props *props = &ma->link_props[rtd->dai_link->id];
> +
> + if (!rtd->dai_link->no_pcm)
> + continue;
> +
> + for_each_rtd_cpu_dais(rtd, i, dai) {
> + ret = macaudio_add_backend_dai_route(card, dai, props->is_speakers);
> +
> + if (ret)
> + return ret;
> + }
> + }
> +
> + if (!ma->mdata) {
> + dev_err(card->dev, "driver doesn't know speaker limits for this model\n");
> + return void_warranty ? 0 : -EINVAL;
> + }
> +
> + macaudio_limit_volume(card, "* Amp Gain", ma->mdata->spk_amp_gain_max);
> + return 0;
> +}
> +
> +static const char * const macaudio_plugin_demux_texts[] = {
> + "Speakers",
> + "Headphones"
> +};
> +
> +SOC_ENUM_SINGLE_VIRT_DECL(macaudio_plugin_demux_enum, macaudio_plugin_demux_texts);
> +
> +static int macaudio_plugin_demux_get(struct snd_kcontrol *kcontrol,
> + struct snd_ctl_elem_value *ucontrol)
> +{
> + struct snd_soc_dapm_context *dapm = snd_soc_dapm_kcontrol_dapm(kcontrol);
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(dapm->card);
> +
> + /*
> + * TODO: Determine what locking is in order here...
> + */
> + ucontrol->value.enumerated.item[0] = ma->jack_plugin_state;
> +
> + return 0;
> +}
> +
> +static int macaudio_jack_event(struct notifier_block *nb, unsigned long event,
> + void *data)
> +{
> + struct snd_soc_jack *jack = data;
> + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(jack->card);
> +
> + ma->jack_plugin_state = !!event;
> +
> + if (!ma->plugin_demux_kcontrol)
> + return 0;
> +
> + snd_soc_dapm_mux_update_power(&ma->card.dapm, ma->plugin_demux_kcontrol,
> + ma->jack_plugin_state,
> + (struct soc_enum *) &macaudio_plugin_demux_enum, NULL);

the term 'plugin' can be understood in many ways by different audio
folks. 'plugin' is usually the term used for processing libraries (VST,
LADSPA, etc). I think here you meant 'jack control'?

> +
> + return 0;
> +}
> +