diff mbox series

[RFC,5/5] ASoC: Add macaudio machine driver

Message ID 20220331000449.41062-6-povik+lin@cutebit.org
State New
Headers show
Series None | expand

Commit Message

Martin Povišer March 31, 2022, 12:04 a.m. UTC
Add ASoC machine driver for Apple Silicon Macs.

Signed-off-by: Martin Povišer <povik+lin@cutebit.org>
---
 sound/soc/apple/Kconfig    |  10 +
 sound/soc/apple/Makefile   |   3 +
 sound/soc/apple/macaudio.c | 597 +++++++++++++++++++++++++++++++++++++
 3 files changed, 610 insertions(+)
 create mode 100644 sound/soc/apple/Kconfig
 create mode 100644 sound/soc/apple/Makefile
 create mode 100644 sound/soc/apple/macaudio.c
diff mbox series

Patch

diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig
new file mode 100644
index 000000000000..afc0243b9309
--- /dev/null
+++ b/sound/soc/apple/Kconfig
@@ -0,0 +1,10 @@ 
+config SND_SOC_APPLE_MACAUDIO
+	tristate "ASoC machine driver for Apple Silicon Macs"
+	depends on ARCH_APPLE || COMPILE_TEST
+	select SND_SOC_APPLE_MCA
+	select SND_SIMPLE_CARD_UTILS
+	select APPLE_ADMAC
+	select COMMON_CLK_APPLE_NCO
+	default ARCH_APPLE
+	help
+	  This option enables an ASoC machine driver for Apple Silicon Macs.
diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile
new file mode 100644
index 000000000000..d7a2df6311b5
--- /dev/null
+++ b/sound/soc/apple/Makefile
@@ -0,0 +1,3 @@ 
+snd-soc-macaudio-objs	:= macaudio.o
+
+obj-$(CONFIG_SND_SOC_APPLE_MACAUDIO)	+= snd-soc-macaudio.o
diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c
new file mode 100644
index 000000000000..3e80f97a9b75
--- /dev/null
+++ b/sound/soc/apple/macaudio.c
@@ -0,0 +1,597 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ASoC machine driver for Apple Silicon Macs
+ *
+ * Copyright (C) The Asahi Linux Contributors
+ *
+ * Based on sound/soc/qcom/{sc7180.c|common.c}
+ *
+ * Copyright (c) 2018, Linaro Limited.
+ * Copyright (c) 2020, The Linux Foundation. All rights reserved.
+ */
+
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <sound/core.h>
+#include <sound/jack.h>
+#include <sound/pcm.h>
+#include <sound/simple_card_utils.h>
+#include <sound/soc.h>
+#include <uapi/linux/input-event-codes.h>
+
+#define DRIVER_NAME "snd-soc-macaudio"
+
+struct macaudio_snd_data {
+	struct snd_soc_card card;
+	struct snd_soc_jack_pin pin;
+	struct snd_soc_jack jack;
+
+	struct macaudio_link_props {
+		unsigned int mclk_fs;
+	} *link_props;
+
+	const struct snd_pcm_chmap_elem *speaker_chmap;
+
+	unsigned int speaker_nchans_array[2];
+	struct snd_pcm_hw_constraint_list speaker_nchans_list;
+
+	struct list_head hidden_kcontrols;
+};
+
+static int macaudio_parse_of(struct macaudio_snd_data *ma, struct snd_soc_card *card)
+{
+	struct device_node *np;
+	struct device_node *codec = NULL;
+	struct device_node *cpu = NULL;
+	struct device *dev = card->dev;
+	struct snd_soc_dai_link *link;
+	struct macaudio_link_props *link_props;
+	int ret, num_links;
+	int i = 0;
+
+	ret = snd_soc_of_parse_card_name(card, "model");
+	if (ret) {
+		dev_err(dev, "Error parsing card name: %d\n", ret);
+		return ret;
+	}
+
+	ret = asoc_simple_parse_routing(card, NULL);
+	if (ret)
+		return ret;
+
+	/* Populate links */
+	num_links = of_get_available_child_count(dev->of_node);
+
+	/* Allocate the DAI link array */
+	card->dai_link = devm_kcalloc(dev, num_links, sizeof(*link), GFP_KERNEL);
+	ma->link_props = devm_kcalloc(dev, num_links, sizeof(*ma->link_props), GFP_KERNEL);
+	if (!card->dai_link || !ma->link_props)
+		return -ENOMEM;
+
+	card->num_links = num_links;
+	link = card->dai_link;
+	link_props = ma->link_props;
+
+	for_each_available_child_of_node(dev->of_node, np) {
+		link->id = i++;
+
+		/* CPU side is bit and frame clock master, I2S with both clocks inverted */
+		link->dai_fmt = SND_SOC_DAIFMT_I2S |
+			SND_SOC_DAIFMT_CBC_CFC |
+			SND_SOC_DAIFMT_GATED |
+			SND_SOC_DAIFMT_IB_IF;
+
+		ret = of_property_read_string(np, "link-name", &link->name);
+		if (ret) {
+			dev_err(card->dev, "Missing link name\n");
+			goto err_put_np;
+		}
+
+		cpu = of_get_child_by_name(np, "cpu");
+		codec = of_get_child_by_name(np, "codec");
+
+		if (!codec || !cpu) {
+			dev_err(dev, "Missing DAI specifications for '%s'\n", link->name);
+			ret = -EINVAL;
+			goto err;
+		}
+
+		ret = snd_soc_of_get_dai_link_codecs(dev, codec, link);
+		if (ret < 0) {
+			if (ret != -EPROBE_DEFER)
+				dev_err(card->dev, "%s: codec dai not found: %d\n",
+					link->name, ret);
+			goto err;
+		}
+
+		ret = snd_soc_of_get_dai_link_cpus(dev, cpu, link);
+		if (ret < 0) {
+			if (ret != -EPROBE_DEFER)
+				dev_err(card->dev, "%s: cpu dai not found: %d\n",
+					link->name, ret);
+			goto err;
+		}
+
+		link->num_platforms = 1;
+		link->platforms	= devm_kzalloc(dev, sizeof(*link->platforms),
+						GFP_KERNEL);
+		if (!link->platforms) {
+			ret = -ENOMEM;
+			goto err;
+		}
+		link->platforms->of_node = link->cpus->of_node;
+
+		of_property_read_u32(np, "mclk-fs", &link_props->mclk_fs);
+
+		link->stream_name = link->name;
+		link++;
+		link_props++;
+
+		of_node_put(cpu);
+		of_node_put(codec);
+	}
+
+	/*
+	 * TODO: Not sure I shouldn't do something about the ->of_node component
+	 * references I leave in dai_link (if successful here).
+	 */
+
+	return 0;
+err:
+	of_node_put(cpu);
+	of_node_put(codec);
+err_put_np:
+	for (i = 0; i < num_links; i++) {
+		snd_soc_of_put_dai_link_codecs(&card->dai_link[i]);
+		snd_soc_of_put_dai_link_cpus(&card->dai_link[i]);
+	}
+	of_node_put(np);
+	return ret;
+}
+
+static int macaudio_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 macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card);
+	struct macaudio_link_props *props = &ma->link_props[rtd->num];
+	struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
+	struct snd_soc_dai *dai;
+	int i, mclk;
+
+	if (props->mclk_fs) {
+		mclk = params_rate(params) * props->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_shutdown(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 macaudio_link_props *props = &ma->link_props[rtd->num];
+	struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0);
+	struct snd_soc_dai *dai;
+	int i;
+
+	if (props->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 bool macaudio_is_speakers(struct snd_soc_dai_link *dai_link)
+{
+	return !strcmp(rtd->dai_link->name, "Speaker")
+		|| !strcmp(rtd->dai_link->name, "Speakers");
+}
+
+static int macaudio_startup(struct snd_pcm_substream *substream)
+{
+	struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream);
+	struct snd_soc_card *card = rtd->card;
+	struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
+	struct snd_pcm_hw_constraint_list *nchans_list = &ma->speaker_nchans_list;
+	unsigned int *nchans_array = ma->speaker_nchans_array;
+	int ret;
+
+	if (macaudio_is_speakers(rtd->dai_link)) {
+		if (rtd->num_codecs > 2) {
+			nchans_list->count = 2;
+			nchans_list->list = nchans_array;
+			nchans_array[0] = 2;
+			nchans_array[1] = rtd->num_codecs;
+
+			ret = snd_pcm_hw_constraint_list(substream->runtime, 0,
+					SNDRV_PCM_HW_PARAM_CHANNELS, nchans_list);
+			if (ret < 0)
+				return ret;
+		} else if (rtd->num_codecs == 2) {
+			ret = snd_pcm_hw_constraint_single(substream->runtime,
+					SNDRV_PCM_HW_PARAM_CHANNELS, 2);
+			if (ret < 0)
+				return ret;
+		}
+	}
+
+	return 0;
+}
+
+static int macaudio_assign_tdm(struct snd_soc_pcm_runtime *rtd)
+{
+	struct snd_soc_card *card = rtd->card;
+	struct snd_soc_dai *dai, *cpu_dai;
+	int ret, i;
+	int nchans = 0, nslots = 0, slot_width = 32;
+
+	nslots = rtd->num_codecs;
+
+	for_each_rtd_codec_dais(rtd, i, dai) {
+		int codec_nchans = 1;
+		int mask = ((1 << codec_nchans) - 1) << nchans;
+
+		ret = snd_soc_dai_set_tdm_slot(dai, mask,
+					mask, nslots, slot_width);
+		if (ret == -EINVAL)
+			/* Try without the RX mask */
+			ret = snd_soc_dai_set_tdm_slot(dai, mask,
+					0, nslots, slot_width);
+
+		if (ret < 0) {
+			dev_err(card->dev, "DAI %s refuses TDM settings: %d",
+					dai->name, ret);
+			return ret;
+		}
+
+		nchans += codec_nchans;
+	}
+
+	cpu_dai = asoc_rtd_to_cpu(rtd, 0);
+	ret = snd_soc_dai_set_tdm_slot(cpu_dai, (1 << nslots) - 1,
+			(1 << nslots) - 1, nslots, slot_width);
+	if (ret < 0) {
+		dev_err(card->dev, "CPU DAI %s refuses TDM settings: %d",
+				cpu_dai->name, ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int macaudio_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 snd_soc_component *component;
+	int ret, i;
+
+	if (rtd->num_codecs > 1) {
+		ret = macaudio_assign_tdm(rtd);
+		if (ret < 0)
+			return ret;
+	}
+
+	for_each_rtd_components(rtd, i, component)
+		snd_soc_component_set_jack(component, &ma->jack, NULL);
+
+	return 0;
+}
+
+static void macaudio_exit(struct snd_soc_pcm_runtime *rtd)
+{
+	struct snd_soc_component *component;
+	int i;
+
+	for_each_rtd_components(rtd, i, component)
+		snd_soc_component_set_jack(component, NULL, NULL);
+}
+
+struct macaudio_kctlfix {
+	char *name;
+	char *value;
+} macaudio_kctlfixes[] = {
+	{"* ASI1 Sel", "Left"},
+	{"* ISENSE Switch", "Off"},
+	{"* VSENSE Switch", "Off"},
+	{ }
+};
+
+static bool macaudio_kctlfix_matches(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 struct macaudio_kctlfix *macaudio_find_kctlfix(const char *name)
+{
+	struct macaudio_kctlfix *fctl;
+
+	for (fctl = macaudio_kctlfixes; fctl->name != NULL; fctl++)
+		if (macaudio_kctlfix_matches(fctl->name, name))
+			return fctl;
+
+	return NULL;
+}
+
+static int macaudio_probe(struct snd_soc_card *card)
+{
+	struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
+	int ret;
+
+	INIT_LIST_HEAD(&ma->hidden_kcontrols);
+
+	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;
+}
+
+/*
+ * Maybe this could be a general ASoC function?
+ */
+static void snd_soc_kcontrol_set_strval(struct snd_soc_card *card,
+				struct snd_kcontrol *kcontrol, const char *strvalue)
+{
+	struct snd_ctl_elem_value value;
+	struct snd_ctl_elem_info info;
+	int sel, i, ret;
+
+	ret = kcontrol->info(kcontrol, &info);
+	if (ret < 0) {
+		dev_err(card->dev, "can't obtain info on control '%s': %d",
+			kcontrol->id.name, ret);
+		return;
+	}
+
+	switch (info.type) {
+	case SNDRV_CTL_ELEM_TYPE_ENUMERATED:
+		for (sel = 0; sel < info.value.enumerated.items; sel++) {
+			info.value.enumerated.item = sel;
+			kcontrol->info(kcontrol, &info);
+
+			if (!strcmp(strvalue, info.value.enumerated.name))
+				break;
+		}
+
+		if (sel == info.value.enumerated.items)
+			goto not_avail;
+
+		for (i = 0; i < info.count; i++)
+			value.value.enumerated.item[i] = sel;
+		break;
+
+	case SNDRV_CTL_ELEM_TYPE_BOOLEAN:
+		sel = !strcmp(strvalue, "On");
+
+		if (!sel && strcmp(strvalue, "Off"))
+			goto not_avail;
+
+		for (i = 0; i < info.count; i++)
+			value.value.integer.value[i] = sel;
+		break;
+
+	case SNDRV_CTL_ELEM_TYPE_INTEGER:
+		if (kstrtoint(strvalue, 10, &sel))
+			goto not_avail;
+
+		for (i = 0; i < info.count; i++)
+			value.value.integer.value[i] = sel;
+		break;
+
+	default:
+		dev_err(card->dev, "%s: control '%s' has unsupported type %d",
+			__func__, kcontrol->id.name, info.type);
+		return;
+	}
+
+	ret = kcontrol->put(kcontrol, &value);
+	if (ret < 0) {
+		dev_err(card->dev, "can't set control '%s' to '%s': %d",
+			kcontrol->id.name, strvalue, ret);
+		return;
+	}
+
+	dev_dbg(card->dev, "set '%s' to '%s'",
+			kcontrol->id.name, strvalue);
+	return;
+
+not_avail:
+	dev_err(card->dev, "option '%s' on control '%s' not available",
+			strvalue, kcontrol->id.name);
+	return;
+
+}
+
+static int macaudio_filter_controls(struct snd_soc_card *card,
+			 struct snd_kcontrol *kcontrol)
+{
+	struct macaudio_kctlfix *fctl = macaudio_find_kctlfix(kcontrol->id.name);
+	struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
+
+	dev_dbg(card->dev, "visiting control %s, have match %d\n",
+		kcontrol->id.name, !!fctl);
+
+	if (!fctl)
+		return 0;
+
+	list_add_tail(&kcontrol->list, &ma->hidden_kcontrols);
+	return 1;
+}
+
+static int macaudio_late_probe(struct snd_soc_card *card)
+{
+	struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
+	struct snd_kcontrol *kcontrol;
+	struct snd_soc_pcm_runtime *rtd;
+	int ret;
+
+	/*
+	 * Here we take it to be okay to fiddle with the kcontrols
+	 * we caught for ourselves.
+	 */
+	list_for_each_entry(kcontrol, &ma->hidden_kcontrols, list) {
+		struct macaudio_kctlfix *fctl = macaudio_find_kctlfix(kcontrol->id.name);
+
+		if (fctl)
+			snd_soc_kcontrol_set_strval(card, kcontrol, fctl->value);
+	}
+
+	for_each_card_rtds(card, rtd) {
+		if (macaudio_is_speakers(rtd->dai_link) && ma->speaker_chmap) {
+			ret = snd_pcm_add_chmap_ctls(rtd->pcm,
+				SNDRV_PCM_STREAM_PLAYBACK, ma->speaker_chmap,
+				rtd->num_codecs, 0, NULL);
+			if (ret < 0)
+				dev_err(card->dev, "failed to add channel map on '%s': %d\n",
+					rtd->dai_link->name, ret);
+		}
+	}
+
+	return 0;
+}
+
+static int macaudio_remove(struct snd_soc_card *card)
+{
+	struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card);
+	struct snd_kcontrol *kcontrol;
+
+	list_for_each_entry(kcontrol, &ma->hidden_kcontrols, list)
+		snd_ctl_free_one(kcontrol);
+
+	return 0;
+}
+
+static const struct snd_soc_ops macaudio_ops = {
+	.startup	= macaudio_startup,
+	.shutdown	= macaudio_shutdown,
+	.hw_params	= macaudio_hw_params,
+};
+
+static const struct snd_soc_dapm_widget macaudio_snd_widgets[] = {
+	SND_SOC_DAPM_HP("Headphones", NULL),
+};
+
+static const struct snd_pcm_chmap_elem macaudio_j274_chmaps[] = {
+	{ .channels = 1,
+	  .map = { SNDRV_CHMAP_MONO } },
+	{ }
+};
+
+static const struct snd_pcm_chmap_elem macaudio_j293_chmaps[] = {
+	{ .channels = 2,
+	  .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR } },
+	{ .channels = 4,
+	  .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR,
+		   SNDRV_CHMAP_RL, SNDRV_CHMAP_RR } },
+	{ }
+};
+
+static const struct snd_pcm_chmap_elem macaudio_j314_chmaps[] = {
+	{ .channels = 2,
+	  .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR } },
+	{ .channels = 6,
+	  .map = { SNDRV_CHMAP_SL, SNDRV_CHMAP_SR,
+		   SNDRV_CHMAP_FL, SNDRV_CHMAP_FR,
+		   SNDRV_CHMAP_RL, SNDRV_CHMAP_RR } },
+	{ }
+};
+
+static const struct of_device_id macaudio_snd_device_id[]  = {
+	{ .compatible = "apple,j274-macaudio", .data = macaudio_j274_chmaps },
+	{ .compatible = "apple,j293-macaudio", .data = macaudio_j293_chmaps },
+	{ .compatible = "apple,j314-macaudio", .data = macaudio_j314_chmaps },
+	{ .compatible = "apple,macaudio", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, macaudio_snd_device_id);
+
+static int macaudio_snd_platform_probe(struct platform_device *pdev)
+{
+	struct snd_soc_card *card;
+	struct macaudio_snd_data *data;
+	struct device *dev = &pdev->dev;
+	struct snd_soc_dai_link *link;
+	const struct of_device_id *of_id;
+	int ret;
+	int i;
+
+	of_id = of_match_device(macaudio_snd_device_id, dev);
+	if (!of_id)
+		return -EINVAL;
+
+	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+	if (!data)
+		return -ENOMEM;
+
+	data->speaker_chmap = of_id->data;
+	card = &data->card;
+	snd_soc_card_set_drvdata(card, data);
+
+	card->owner = THIS_MODULE;
+	card->driver_name = DRIVER_NAME;
+	card->dev = dev;
+	card->dapm_widgets = macaudio_snd_widgets;
+	card->num_dapm_widgets = ARRAY_SIZE(macaudio_snd_widgets);
+	card->probe = macaudio_probe;
+	card->late_probe = macaudio_late_probe;
+	card->remove = macaudio_remove;
+	card->filter_controls = macaudio_filter_controls;
+	card->remove = macaudio_remove;
+
+	ret = macaudio_parse_of(data, card);
+	if (ret)
+		return ret;
+
+	for_each_card_prelinks(card, i, link) {
+		link->ops = &macaudio_ops;
+		link->init = macaudio_init;
+		link->exit = macaudio_exit;
+	}
+
+	return devm_snd_soc_register_card(dev, card);
+}
+
+static struct platform_driver macaudio_snd_driver = {
+	.probe = macaudio_snd_platform_probe,
+	.driver = {
+		.name = DRIVER_NAME,
+		.of_match_table = macaudio_snd_device_id,
+		.pm = &snd_soc_pm_ops,
+	},
+};
+module_platform_driver(macaudio_snd_driver);
+
+MODULE_AUTHOR("Martin Povišer <povik+lin@cutebit.org>");
+MODULE_DESCRIPTION("Apple Silicon Macs machine-level sound driver");
+MODULE_LICENSE("GPL");