[alsa-devel] [PATCH 4/4] ASoC: firmware core: Add core support to create and destroy firmware components.

Liam Girdwood lrg at ti.com
Mon Nov 19 19:12:45 CET 2012


Add generic support to create ASoC kcontrols, widgets, graphs and coefficients from
firmware files.  This is useful for many modern audio DSP devices where the firmware
often defines the graph and mixers and allows the vendor firmware file to ship with the
ASoC specific components along with the firmware text.

The generic FW format is very simple and uses a small header to describe each file
section. e.g. a FW file with text and kcontrols would have a header for the text and
another header for the kcontrols section. The sections can be in any order within
the firmware file meaning many different FW section files can be cat'ed together into
a larger file.

This patch creates the generic ASoC firmware core and an associated header API
that can also be used by userspace firmware generation tools. It also defines a
small API that can be used by ASoc component drivers to interrogate and process
the firmware data. Most firmware data is handled by the core, e.g. widgets, kcontrols
and graph, whilst other data is passed directly to the component driver for
enumeration e.g. text.

This patch has working support to load ASoC :-

 o Kcontrols
 o TLV Kcontrols
 o DAPM widgets
 o DAPM Graph
 o Kcontrols with coefficients.
 o Firmware Text
 o Custom vendor blocks

There is a command line firmware file generation tool that is mostly complete at :-

git at gitorious.org:omap-audio/asoc-fw.git

This tool generates the headers and generic components based on the ASoC types above
and it's intended to be upstreamed in to alsa-utils soon. There is example code with
the tool for generating firmware for the OMAP4 ABE.

Signed-off-by: Liam Girdwood <lrg at ti.com>
---
 include/sound/soc-fw.h |  270 +++++++++
 sound/soc/Makefile     |    2 +-
 sound/soc/soc-core.c   |    4 +
 sound/soc/soc-dapm.c   |    6 +
 sound/soc/soc-fw.c     | 1505 ++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1786 insertions(+), 1 deletion(-)
 create mode 100644 include/sound/soc-fw.h
 create mode 100644 sound/soc/soc-fw.c

diff --git a/include/sound/soc-fw.h b/include/sound/soc-fw.h
new file mode 100644
index 0000000..ada25b5
--- /dev/null
+++ b/include/sound/soc-fw.h
@@ -0,0 +1,270 @@
+/*
+ * linux/sound/soc-fw.h -- ALSA SoC Firmware Controls and DAPM
+ *
+ * Copyright:	2012 Texas Instruments Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * Simple file API to load FW that includes mixers, coefficients, DAPM graphs,
+ * algorithms, equalisers, DAIs, widgets etc.
+ */
+
+#ifndef __LINUX_SND_SOC_FW_H
+#define __LINUX_SND_SOC_FW_H
+
+/* Header magic number and string sizes */
+#define SND_SOC_FW_MAGIC	0x41536F43 /* ASoC */
+#define SND_SOC_FW_TEXT_SIZE	32
+#define SND_SOC_FW_NUM_TEXTS	16
+
+/*
+ * File andBlock header data types.
+ * Add new generic and vendor types to end of list.
+ * Generic types are handled by the core whilst vendors types are passed
+ * to the component drivers for handling.
+ */
+#define SND_SOC_FW_MIXER		1
+#define SND_SOC_FW_DAPM_GRAPH		2
+#define SND_SOC_FW_DAPM_PINS		3
+#define SND_SOC_FW_DAPM_WIDGET		4
+#define SND_SOC_FW_DAI_LINK		5
+#define SND_SOC_FW_COEFF		6
+
+#define SND_SOC_FW_VENDOR_FW		1000
+#define SND_SOC_FW_VENDOR_CONFIG	1001
+#define SND_SOC_FW_VENDOR_COEFF	1002
+#define SND_SOC_FW_VENDOR_CODEC	1003
+
+struct firmware;
+
+/*
+ * File and Block Header
+ */
+struct snd_soc_fw_hdr {
+	u32 magic;
+	u32 type;
+	u32 vendor_type; /* optional vendor specific type info */
+	u32 version; /* optional vendor specific version details */
+	u32 size; /* data bytes, excluding this header */
+	/* file data contents start here */
+};
+
+
+struct snd_soc_fw_ctl_tlv {
+	u32 numid;	/* control element numeric identification */
+	u32 length;	/* in bytes aligned to 4 */
+	/* tlv data starts here */
+};
+
+struct snd_soc_fw_control_hdr {
+	char name[SND_SOC_FW_TEXT_SIZE];
+	u32 index;
+	u32 access;
+	u32 tlv_size;
+};
+
+/*
+ * Mixer KControl.
+ */
+struct snd_soc_fw_mixer_control {
+	struct snd_soc_fw_control_hdr hdr;
+	s32 min;
+	s32 max;
+	s32 platform_max;
+	u32 reg;
+	u32 rreg;
+	u32 shift;
+	u32 rshift;
+	u32 invert;
+};
+
+/*
+ * Enumerated KControl
+ */
+struct snd_soc_fw_enum_control {
+	struct snd_soc_fw_control_hdr hdr;
+	u32 reg;
+	u32 reg2;
+	u32 shift_l;
+	u32 shift_r;
+	u32 max;
+	u32 mask;
+	union {	/* both texts and values are the same size */
+		char texts[SND_SOC_FW_NUM_TEXTS][SND_SOC_FW_TEXT_SIZE];
+		u32 values[SND_SOC_FW_NUM_TEXTS * SND_SOC_FW_TEXT_SIZE / 4];
+	};
+};
+
+/*
+ * Kcontrol Header
+ */
+struct snd_soc_fw_kcontrol {
+	u32 count; /* in kcontrols (based on type) */
+	/* kcontrols here */
+};
+
+/*
+ * DAPM Graph Element
+ */
+struct snd_soc_fw_dapm_graph_elem {
+	char sink[SND_SOC_FW_TEXT_SIZE];
+	char control[SND_SOC_FW_TEXT_SIZE];
+	char source[SND_SOC_FW_TEXT_SIZE];
+};
+
+/*
+ * DAPM Pin Element.
+ */
+struct snd_soc_fw_dapm_pin_elem {
+	char name[SND_SOC_FW_TEXT_SIZE];
+	u32 disconnect:1;
+	u32 ignore_suspend:1;
+};
+
+
+/*
+ * DAPM Widget.
+ */
+struct snd_soc_fw_dapm_widget {
+	u32 id;		/* snd_soc_dapm_type */
+	char name[SND_SOC_FW_TEXT_SIZE];
+	char sname[SND_SOC_FW_TEXT_SIZE];
+
+	s32 reg;		/* negative reg = no direct dapm */
+	u32 shift;		/* bits to shift */
+	u32 mask;		/* non-shifted mask */
+	u32 invert:1;		/* invert the power bit */
+	u32 ignore_suspend:1;	/* kept enabled over suspend */
+
+	/* kcontrols that relate to this widget */
+	struct snd_soc_fw_kcontrol kcontrol;
+	/* controls follow here */
+};
+
+/*
+ * DAPM Graph and Pins.
+ */
+struct snd_soc_fw_dapm_elems {
+	u32 count; /* in elements */
+	/* elements here */
+};
+
+/*
+ * Coeffcient File Data.
+ */
+struct snd_soc_file_coeff_data {
+	u32 count; /* in elems */
+	u32 size;	/* total data size */
+	u32 id; /* associated mixer ID */
+	/* data here */
+};
+
+#ifdef __KERNEL__
+
+/*
+ * Kcontrol operations - used to map handlers onto firmware based controls.
+ */
+struct snd_soc_fw_kcontrol_ops {
+	u32 id;
+	int (*get)(struct snd_kcontrol *kcontrol,
+			struct snd_ctl_elem_value *ucontrol);
+	int (*put)(struct snd_kcontrol *kcontrol,
+			struct snd_ctl_elem_value *ucontrol);
+	int (*info)(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_info *uinfo);
+};
+
+/*
+ * Public API - Used by component drivers to load new mixers, DAPM, vendor
+ * specific data.
+ */
+struct snd_soc_fw_codec_ops {
+
+	/* external kcontrol init - can be used to set ext funcs + pdata */
+	int (*control_load) (struct snd_soc_codec *, struct snd_kcontrol_new *);
+
+	/* external widget init - can be used to set ext funcs + pdata */
+	int (*widget_load) (struct snd_soc_codec *, struct snd_soc_dapm_widget *);
+
+	/* callback to handle vendor data */
+	int (*vendor_load) (struct snd_soc_codec *, struct snd_soc_fw_hdr *);
+	int (*vendor_unload) (struct snd_soc_codec *, struct snd_soc_fw_hdr *);
+
+	/* completion - called at completion of firmware loading */
+	void (*complete) (struct snd_soc_codec *);
+
+	/* kcontrols operations */
+	const struct snd_soc_fw_kcontrol_ops *io_ops;
+	int io_ops_count;
+};
+
+struct snd_soc_fw_platform_ops {
+
+	/* external kcontrol init - can be used to set ext funcs + pdata */
+	int (*control_load) (struct snd_soc_platform *, struct snd_kcontrol_new *);
+
+	/* external widget init - can be used to set ext funcs + pdata */
+	int (*widget_load) (struct snd_soc_platform *, struct snd_soc_dapm_widget *);
+
+	/* callback to handle vendor data */
+	int (*vendor_load) (struct snd_soc_platform *, struct snd_soc_fw_hdr *);
+	int (*vendor_unload) (struct snd_soc_platform *, struct snd_soc_fw_hdr *);
+
+	/* completion - called at completion of firmware loading */
+	void (*complete) (struct snd_soc_platform *);
+
+	/* kcontrols operations */
+	const struct snd_soc_fw_kcontrol_ops *io_ops;
+	int io_ops_count;
+};
+
+struct snd_soc_fw_card_ops {
+
+	/* external kcontrol init - can be used to set ext funcs + pdata */
+	int (*control_load) (struct snd_soc_card *, struct snd_kcontrol_new *);
+
+	/* external widget init - can be used to set ext funcs + pdata */
+	int (*widget_load) (struct snd_soc_card *, struct snd_soc_dapm_widget *);
+
+	/* callback to handle vendor data */
+	int (*vendor_load) (struct snd_soc_card *, struct snd_soc_fw_hdr *);
+	int (*vendor_unload) (struct snd_soc_card *, struct snd_soc_fw_hdr *);
+
+	/* completion */
+	void (*complete) (struct snd_soc_card *);
+
+	/* kcontrols operations */
+	const struct snd_soc_fw_kcontrol_ops *io_ops;
+	int io_ops_count;
+};
+
+/* gets a pointer to data from the firmware block header */
+static inline const void *snd_soc_fw_get_data(struct snd_soc_fw_hdr *hdr)
+{
+	const void *ptr = hdr;
+
+	return ptr + sizeof(*hdr);
+}
+
+/* Firmware loading for component drivers */
+int snd_soc_fw_load_card(struct snd_soc_card *card,
+	struct snd_soc_fw_card_ops *ops, const struct firmware *fw);
+int snd_soc_fw_load_platform(struct snd_soc_platform *platform,
+	struct snd_soc_fw_platform_ops *ops, const struct firmware *fw);
+int snd_soc_fw_load_codec(struct snd_soc_codec *codec,
+	struct snd_soc_fw_codec_ops *ops, const struct firmware *fw);
+
+/* Firmware based dynamic widget and assoc kcontrol removal */
+void snd_soc_fw_dcontrols_remove_widgets(struct snd_soc_dapm_context *dapm);
+void snd_soc_fw_dcontrols_remove_widget(struct snd_soc_dapm_widget *w);
+
+/* Firmware based dynamic kcontrol removal for components */
+void snd_soc_fw_dcontrols_remove_codec(struct snd_soc_codec *codec);
+void snd_soc_fw_dcontrols_remove_platform(struct snd_soc_platform *platform);
+void snd_soc_fw_dcontrols_remove_card(struct snd_soc_card *soc_card);
+int snd_soc_fw_dcontrols_remove_all(struct snd_soc_card *soc_card);
+
+#endif
+#endif
diff --git a/sound/soc/Makefile b/sound/soc/Makefile
index 99f32f7..4df20e5 100644
--- a/sound/soc/Makefile
+++ b/sound/soc/Makefile
@@ -1,5 +1,5 @@
 snd-soc-core-objs := soc-core.o soc-dapm.o soc-jack.o soc-cache.o soc-utils.o
-snd-soc-core-objs += soc-pcm.o soc-compress.o soc-io.o
+snd-soc-core-objs += soc-pcm.o soc-compress.o soc-io.o soc-fw.o
 
 ifneq ($(CONFIG_SND_SOC_DMAENGINE_PCM),)
 snd-soc-core-objs += soc-dmaengine-pcm.o
diff --git a/sound/soc/soc-core.c b/sound/soc/soc-core.c
index 1f448ab..614bb5b 100644
--- a/sound/soc/soc-core.c
+++ b/sound/soc/soc-core.c
@@ -40,6 +40,7 @@
 #include <sound/pcm_params.h>
 #include <sound/soc.h>
 #include <sound/soc-dpcm.h>
+#include <sound/soc-fw.h>
 #include <sound/initval.h>
 
 #define CREATE_TRACE_POINTS
@@ -1881,6 +1882,9 @@ static int soc_cleanup_card_resources(struct snd_soc_card *card)
 	/* remove and free each DAI */
 	soc_remove_dai_links(card);
 
+	/* remove any dynamic kcontrols */
+	snd_soc_fw_dcontrols_remove_all(card);
+
 	soc_cleanup_card_debugfs(card);
 
 	/* remove the card */
diff --git a/sound/soc/soc-dapm.c b/sound/soc/soc-dapm.c
index f456378..84ff20b 100644
--- a/sound/soc/soc-dapm.c
+++ b/sound/soc/soc-dapm.c
@@ -41,6 +41,7 @@
 #include <sound/pcm.h>
 #include <sound/pcm_params.h>
 #include <sound/soc.h>
+#include <sound/soc-fw.h>
 #include <sound/initval.h>
 
 #include <trace/events/asoc.h>
@@ -2093,6 +2094,11 @@ static void dapm_free_widgets(struct snd_soc_dapm_context *dapm)
 			kfree(p->long_name);
 			kfree(p);
 		}
+
+		/* check and free and dynamic widget kcontrols */
+		if (w->denum || w->dmixer)
+			snd_soc_fw_dcontrols_remove_widget(w);
+
 		kfree(w->kcontrols);
 		kfree(w->name);
 		kfree(w);
diff --git a/sound/soc/soc-fw.c b/sound/soc/soc-fw.c
new file mode 100644
index 0000000..e9144aa
--- /dev/null
+++ b/sound/soc/soc-fw.c
@@ -0,0 +1,1505 @@
+/*
+ * soc-fw.c  --  ALSA SoC Firmware
+ *
+ * Copyright (C) 2012 Texas Instruments Inc.
+ *
+ * Author: Liam Girdwood <lrg at ti.com>
+ *
+ *  This program is free software; you can redistribute  it and/or modify it
+ *  under  the terms of  the GNU General  Public License as published by the
+ *  Free Software Foundation;  either version 2 of the  License, or (at your
+ *  option) any later version.
+ *
+ *  Support for audio fimrware to contain kcontrols, DAPM graphs, widgets,
+ *  DAIs, equalizers, firmware, coefficienst etc.
+ *
+ *  This file only manages the DAPM and Kcontrol components, all other firmware
+ *  data is passed to component drivers for bespoke handling.
+ */
+
+#include <linux/kernel.h>
+#include <linux/export.h>
+#include <linux/list.h>
+#include <linux/firmware.h>
+#include <linux/slab.h>
+#include <sound/soc.h>
+#include <sound/soc-dapm.h>
+#include <sound/soc-fw.h>
+
+/*
+ * We make several passes over the data (since it wont necessarily be ordered)
+ * and process objects in the following order. This guarantees the component
+ * drivers will be ready with any vendor data before the mixers and DAPM objects
+ * are loaded (that may make use of the vendor data).
+ */
+#define SOC_FW_PASS_VENDOR	0
+#define SOC_FW_PASS_MIXER	1
+#define SOC_FW_PASS_COEFF	SOC_FW_PASS_MIXER
+#define SOC_FW_PASS_WIDGET	2
+#define SOC_FW_PASS_GRAPH	3
+#define SOC_FW_PASS_PINS	4
+
+#define SOC_FW_PASS_START	SOC_FW_PASS_VENDOR
+#define SOC_FW_PASS_END	SOC_FW_PASS_PINS
+
+struct soc_fw {
+	const char *file;
+	const struct firmware *fw;
+
+	/* runtime FW parsing */
+	const u8 *pos;		/* read postion */
+	const u8 *hdr_pos;	/* header position */
+	unsigned int pass;	/* pass number */
+
+	/* component caller */
+	struct device *dev;
+	struct snd_soc_codec *codec;
+	struct snd_soc_platform *platform;
+	struct snd_soc_card *card;
+
+	/* kcontrol operations */
+	const struct snd_soc_fw_kcontrol_ops *io_ops;
+	int io_ops_count;
+
+	/* optional fw loading callbacks to component drivers */
+	union {
+		struct snd_soc_fw_codec_ops *codec_ops;
+		struct snd_soc_fw_platform_ops *platform_ops;
+		struct snd_soc_fw_card_ops *card_ops;
+	};
+};
+
+static int soc_fw_process_headers(struct soc_fw *sfw);
+static void soc_fw_complete(struct soc_fw *sfw);
+
+/* List of Kcontrol types and associated operations. */
+static const struct snd_soc_fw_kcontrol_ops io_ops[] = {
+	{SOC_CONTROL_IO_VOLSW, snd_soc_get_volsw,
+		snd_soc_put_volsw, snd_soc_info_volsw},
+	{SOC_CONTROL_IO_VOLSW_SX, snd_soc_get_volsw_sx,
+		snd_soc_put_volsw_sx, NULL},
+	{SOC_CONTROL_IO_VOLSW_S8, snd_soc_get_volsw_s8,
+		snd_soc_put_volsw_s8, snd_soc_info_volsw_s8},
+	{SOC_CONTROL_IO_ENUM, snd_soc_get_enum_double,
+		snd_soc_put_enum_double, snd_soc_info_enum_double},
+	{SOC_CONTROL_IO_ENUM_EXT, NULL,
+		NULL, snd_soc_info_enum_ext},
+	{SOC_CONTROL_IO_BYTES, snd_soc_bytes_get,
+		snd_soc_bytes_put, snd_soc_bytes_info},
+	{SOC_CONTROL_IO_BOOL_EXT, NULL,
+		NULL, snd_ctl_boolean_mono_info},
+	{SOC_CONTROL_IO_ENUM_VALUE, snd_soc_get_value_enum_double,
+		snd_soc_put_value_enum_double, NULL},
+	{SOC_CONTROL_IO_RANGE, snd_soc_get_volsw_range,
+		snd_soc_put_volsw_range, snd_soc_info_volsw_range},
+	{SOC_CONTROL_IO_VOLSW_XR_SX, snd_soc_get_xr_sx,
+		snd_soc_put_xr_sx, snd_soc_info_xr_sx},
+	{SOC_CONTROL_IO_STROBE, snd_soc_get_strobe,
+		snd_soc_put_strobe, NULL},
+
+	{SOC_DAPM_IO_VOLSW, snd_soc_dapm_get_volsw,
+		snd_soc_dapm_put_volsw, NULL},
+	{SOC_DAPM_IO_ENUM_DOUBLE, snd_soc_dapm_get_enum_double,
+		snd_soc_dapm_put_enum_double, snd_soc_info_enum_double},
+	{SOC_DAPM_IO_ENUM_VIRT, snd_soc_dapm_get_enum_virt,
+		snd_soc_dapm_put_enum_virt, NULL},
+	{SOC_DAPM_IO_ENUM_VALUE, snd_soc_dapm_get_value_enum_double,
+		snd_soc_dapm_put_value_enum_double, NULL},
+	{SOC_DAPM_IO_PIN, snd_soc_dapm_get_pin_switch,
+		snd_soc_dapm_put_pin_switch, snd_soc_dapm_info_pin_switch},
+};
+
+static inline void soc_fw_list_add_enum(struct soc_fw *sfw, struct soc_enum *se)
+{
+	if (sfw->codec)
+		list_add(&se->list, &sfw->codec->denums);
+	else if (sfw->platform)
+		list_add(&se->list, &sfw->platform->denums);
+	else if (sfw->card)
+		list_add(&se->list, &sfw->card->denums);
+}
+
+static inline void soc_fw_list_add_mixer(struct soc_fw *sfw,
+	struct soc_mixer_control *mc)
+{
+	if (sfw->codec)
+		list_add(&mc->list, &sfw->codec->dmixers);
+	else if (sfw->platform)
+		list_add(&mc->list, &sfw->platform->dmixers);
+	else if (sfw->card)
+		list_add(&mc->list, &sfw->card->dmixers);
+}
+
+static inline struct snd_soc_dapm_context *soc_fw_dapm_get(struct soc_fw *sfw)
+{
+	if (sfw->codec)
+		return &sfw->codec->dapm;
+	else if (sfw->platform)
+		return &sfw->platform->dapm;
+	else if (sfw->card)
+		return &sfw->card->dapm;
+	BUG();
+}
+
+static inline struct snd_soc_card *soc_fw_card_get(struct soc_fw *sfw)
+{
+	if (sfw->codec)
+		return sfw->codec->card;
+	else if (sfw->platform)
+		return sfw->platform->card;
+	else if (sfw->card)
+		return sfw->card;
+	BUG();
+}
+
+/* check we dont overflow the data for this control chunk */
+static int soc_fw_check_control_count(struct soc_fw *sfw, size_t elem_size,
+	unsigned int count, size_t bytes)
+{
+	const u8 *end = sfw->pos + elem_size * count;
+
+	if (end > sfw->fw->data + sfw->fw->size) {
+		dev_err(sfw->dev, "ASoC: controls overflow end of data\n");
+		return -EINVAL;
+	}
+
+	/* check there is enough room in chunk for control.
+	   extra bytes at the end of control are for vendor data here  */
+	if (elem_size * count > bytes) {
+		dev_err(sfw->dev, "ASoC: controls count %d of elem size %d "
+			"are bigger than chunk %d\n", count, elem_size, bytes);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static inline int soc_fw_is_eof(struct soc_fw *sfw)
+{
+	const u8 *end = sfw->hdr_pos;
+
+	if (end >= sfw->fw->data + sfw->fw->size)
+		return 1;
+	return 0;
+}
+
+static inline unsigned int soc_fw_get_hdr_offset(struct soc_fw *sfw)
+{
+	return (unsigned int)(sfw->hdr_pos - sfw->fw->data);
+}
+
+static inline unsigned int soc_fw_get_offset(struct soc_fw *sfw)
+{
+	return (unsigned int)(sfw->pos - sfw->fw->data);
+}
+
+/* pass vendor data to component driver for processing */
+static int soc_fw_vendor_load_(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	int ret = 0;
+
+	if (sfw->codec && sfw->codec_ops && sfw->codec_ops->vendor_load)
+		ret = sfw->codec_ops->vendor_load(sfw->codec, hdr);
+
+	if (sfw->platform && sfw->platform_ops && sfw->platform_ops->vendor_load)
+		ret = sfw->platform_ops->vendor_load(sfw->platform, hdr);
+
+	if (sfw->card && sfw->card_ops && sfw->card_ops->vendor_load)
+		ret = sfw->card_ops->vendor_load(sfw->card, hdr);
+
+	if (ret < 0)
+		dev_err(sfw->dev, "ASoC: vendor load failed at hdr offset"
+			" %d/0x%x for type %d:%d\n", soc_fw_get_hdr_offset(sfw),
+			soc_fw_get_hdr_offset(sfw), hdr->type, hdr->vendor_type);
+	return ret;
+}
+
+/* pass vendor data to component driver for processing */
+static int soc_fw_vendor_load(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	if (sfw->pass != SOC_FW_PASS_VENDOR)
+		return 0;
+
+	return soc_fw_vendor_load_(sfw, hdr);
+}
+
+/* pass new dynamic widget to component driver. mainly for external widgets */
+static int soc_fw_widget_load(struct soc_fw *sfw, struct snd_soc_dapm_widget *w)
+{
+	if (sfw->codec && sfw->codec_ops && sfw->codec_ops->widget_load)
+		return sfw->codec_ops->widget_load(sfw->codec, w);
+
+	if (sfw->platform && sfw->platform_ops && sfw->platform_ops->widget_load)
+		return sfw->platform_ops->widget_load(sfw->platform, w);
+
+	if (sfw->card && sfw->card_ops && sfw->card_ops->widget_load)
+		return sfw->card_ops->widget_load(sfw->card, w);
+
+	dev_info(sfw->dev, "ASoC: no handler specified for ext widget %s\n",
+		w->name);
+	return 0;
+}
+
+/* tell the component driver that all firmware has been loaded in this request */
+static void soc_fw_complete(struct soc_fw *sfw)
+{
+	if (sfw->codec && sfw->codec_ops && sfw->codec_ops->complete)
+		sfw->codec_ops->complete(sfw->codec);
+	if (sfw->platform && sfw->platform_ops && sfw->platform_ops->complete)
+		sfw->platform_ops->complete(sfw->platform);
+	if (sfw->card && sfw->card_ops && sfw->card_ops->complete)
+		sfw->card_ops->complete(sfw->card);
+}
+
+/* add a dynamic kcontrol */
+static int soc_fw_add_dcontrol(struct snd_card *card, struct device *dev,
+	const struct snd_kcontrol_new *control_new, const char *prefix,
+	void *data, struct snd_kcontrol **kcontrol)
+{
+	int err;
+
+	*kcontrol = snd_soc_cnew(control_new, data, control_new->name, prefix);
+	if (*kcontrol == NULL) {
+		dev_err(dev, "ASoC: Failed to create new kcontrol %s\n",
+		control_new->name);
+		return -ENOMEM;
+	}
+
+	err = snd_ctl_add(card, *kcontrol);
+	if (err < 0) {
+		kfree(*kcontrol);
+		dev_err(dev, "ASoC: Failed to add %s: %d\n", control_new->name,
+			err);
+		return err;
+	}
+
+	return 0;
+}
+
+/* add a dynamic kcontrol for component driver */
+static int soc_fw_add_kcontrol(struct soc_fw *sfw, struct snd_kcontrol_new *k,
+	struct snd_kcontrol **kcontrol)
+{
+	if (sfw->codec) {
+		struct snd_soc_codec *codec = sfw->codec;
+
+		return soc_fw_add_dcontrol(codec->card->snd_card, codec->dev,
+				k, codec->name_prefix, codec, kcontrol);
+	} else if (sfw->platform) {
+		struct snd_soc_platform *platform = sfw->platform;
+
+		return soc_fw_add_dcontrol(platform->card->snd_card,
+				platform->dev, k, NULL, platform, kcontrol);
+	} else if (sfw->card) {
+		struct snd_soc_card *card = sfw->card;
+
+		return soc_fw_add_dcontrol(card->snd_card, card->dev,
+				k, NULL, card, kcontrol);
+	} else
+		dev_info(sfw->dev, "ASoC: no handler specified for kcontrol %s\n",
+			k->name);
+	return 0;
+}
+
+/* bind a kcontrol to it's IO handlers */
+static int soc_fw_kcontrol_bind_io(u32 io_type, struct snd_kcontrol_new *k,
+	const struct snd_soc_fw_kcontrol_ops *ops, int num_ops)
+{
+	int i;
+
+	for (i = 0; i < num_ops; i++) {
+
+		if (SOC_CONTROL_GET_ID_PUT(ops[i].id) ==
+			SOC_CONTROL_GET_ID_PUT(io_type) && ops[i].put)
+			k->put = ops[i].put;
+		if (SOC_CONTROL_GET_ID_GET(ops[i].id) ==
+			SOC_CONTROL_GET_ID_GET(io_type) && ops[i].get)
+			k->get = ops[i].get;
+		if (SOC_CONTROL_GET_ID_INFO(ops[i].id) ==
+			SOC_CONTROL_GET_ID_INFO(io_type) && ops[i].info)
+			k->info = ops[i].info;
+	}
+
+	/* let the caller know if we need to bind external kcontrols */
+	if (!k->put || !k->get || !k->info)
+		return 1;
+
+	return 0;
+}
+
+/* optionally pass new dynamic kcontrol to component driver. */
+static int soc_fw_init_kcontrol(struct soc_fw *sfw, struct snd_kcontrol_new *k)
+{
+	if (sfw->codec && sfw->codec_ops && sfw->codec_ops->control_load)
+		return sfw->codec_ops->control_load(sfw->codec, k);
+
+	if (sfw->platform && sfw->platform_ops && sfw->platform_ops->control_load)
+		return sfw->platform_ops->control_load(sfw->platform, k);
+
+	if (sfw->card && sfw->card_ops && sfw->card_ops->control_load)
+		return sfw->card_ops->control_load(sfw->card, k);
+
+	dev_info(sfw->dev, "ASoC: no handler specified for kcontrol %s\n",
+		k->name);
+	return 0;
+}
+
+static int soc_fw_create_tlv(struct soc_fw *sfw, struct snd_kcontrol_new *kc,
+	u32 tlv_size)
+{
+	struct snd_soc_fw_ctl_tlv *fw_tlv;
+	struct snd_ctl_tlv *tlv;
+
+	if (tlv_size == 0)
+		return 0;
+
+	fw_tlv = (struct snd_soc_fw_ctl_tlv *) sfw->pos;
+	sfw->pos += tlv_size;
+
+	tlv = kzalloc(sizeof(*tlv) + tlv_size, GFP_KERNEL);
+	if (tlv == NULL)
+		return -ENOMEM;
+
+	dev_dbg(sfw->dev, " created TLV type %d size %d bytes\n",
+		fw_tlv->numid, fw_tlv->length);
+	tlv->numid = fw_tlv->numid;
+	tlv->length = fw_tlv->length;
+	memcpy(tlv->tlv, fw_tlv + 1, fw_tlv->length);
+	kc->tlv.p = (void*)tlv;
+
+	return 0;
+}
+
+static inline void soc_fw_free_tlv(struct soc_fw *sfw,
+	struct snd_kcontrol_new *kc)
+{
+	kfree (kc->tlv.p);
+}
+
+static int soc_fw_dmixer_create(struct soc_fw *sfw, unsigned int count,
+	size_t size)
+{
+	struct snd_soc_fw_mixer_control *mc;
+	struct soc_mixer_control *sm;
+	struct snd_kcontrol_new kc;
+	int i, err, ext;
+
+	if (soc_fw_check_control_count(sfw,
+		sizeof(struct snd_soc_fw_mixer_control), count, size)) {
+		dev_err(sfw->dev, "ASoC: invalid count %d for controls\n", count);
+		return -EINVAL;
+	}
+
+	for (i = 0; i < count; i++) {
+		mc = (struct snd_soc_fw_mixer_control*)sfw->pos;
+		sfw->pos += sizeof(struct snd_soc_fw_mixer_control);
+
+		/* validate kcontrol */
+		if (strnlen(mc->hdr.name, SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE)
+			return -EINVAL;
+
+		sm = kzalloc(sizeof(*sm), GFP_KERNEL);
+		if (!sm)
+			return -ENOMEM;
+
+		dev_dbg(sfw->dev, "ASoC: adding mixer kcontrol %s with access"
+			" 0x%x\n", mc->hdr.name, mc->hdr.access);
+
+		memset(&kc, 0, sizeof(kc));
+		kc.name = mc->hdr.name;
+		kc.private_value = (long)sm;
+		kc.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+		kc.access = mc->hdr.access;
+
+		sm->reg = mc->reg;
+		sm->rreg = mc->rreg;
+		sm->shift = mc->shift;
+		sm->rshift = mc->rshift;
+		sm->max = mc->max;
+		sm->min = mc->min;
+		sm->invert = mc->invert;
+		sm->platform_max = mc->platform_max;
+		INIT_LIST_HEAD(&sm->list);
+
+		/* map standard io handlers and check for external handlers */
+		ext = soc_fw_kcontrol_bind_io(mc->hdr.index, &kc, io_ops,
+			ARRAY_SIZE(io_ops));
+		if (ext) {
+			/* none exist, so now try and map ext handlers */
+			ext = soc_fw_kcontrol_bind_io(mc->hdr.index, &kc,
+				sfw->io_ops, sfw->io_ops_count);
+			if (ext) {
+				dev_err(sfw->dev, "ASoC: no complete mixer IO"
+					"handler for %s type (g,p,i) %d:%d:%d\n",
+					mc->hdr.name,
+					SOC_CONTROL_GET_ID_GET(mc->hdr.index),
+					SOC_CONTROL_GET_ID_PUT(mc->hdr.index),
+					SOC_CONTROL_GET_ID_INFO(mc->hdr.index));
+				kfree(sm);
+				continue;
+			}
+
+			err = soc_fw_init_kcontrol(sfw, &kc);
+			if (err < 0) {
+				dev_err(sfw->dev, "ASoC: failed to init %s\n",
+					mc->hdr.name);
+				kfree(sm);
+				continue;
+			}
+		}
+
+		/* create any TLV data */
+		soc_fw_create_tlv(sfw, &kc, mc->hdr.tlv_size);
+
+		/* register control here */
+		err = soc_fw_add_kcontrol(sfw, &kc, &sm->dcontrol);
+		if (err < 0) {
+			dev_err(sfw->dev, "ASoC: failed to add %s\n", mc->hdr.name);
+			soc_fw_free_tlv(sfw, &kc);
+			kfree(sm);
+			continue;
+		}
+
+		soc_fw_list_add_mixer(sfw, sm);
+	}
+
+	return 0;
+}
+
+static inline void soc_fw_denum_free_data(struct soc_enum *se)
+{
+	int i;
+
+	if (se->dvalues)
+		kfree(se->dvalues);
+	else {
+		for (i = 0; i < se->max - 1; i++)
+			kfree(se->dtexts[i]);
+	}
+}
+
+static int soc_fw_denum_create_texts(struct soc_enum *se,
+	struct snd_soc_fw_enum_control *ec)
+{
+	int i, ret;
+
+	se->dtexts = kzalloc(sizeof(char *) * ec->max, GFP_KERNEL);
+	if (se->dtexts == NULL)
+		return -ENOMEM;
+
+	for (i = 0; i < ec->max; i++) {
+
+		if (strnlen(ec->texts[i], SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE) {
+			ret = -EINVAL;
+			goto err;
+		}
+
+		se->dtexts[i] = kstrdup(ec->texts[i], GFP_KERNEL);
+		if (!se->dtexts[i]) {
+			ret = -ENOMEM;
+			goto err;
+		}
+	}
+
+	return 0;
+
+err:
+	for (--i; i >= 0; i--)
+		kfree(se->dtexts[i]);
+	kfree(se->dtexts);
+	return ret;
+}
+
+static int soc_fw_denum_create_values(struct soc_enum *se,
+	struct snd_soc_fw_enum_control *ec)
+{
+	if (ec->max > sizeof(*ec->values))
+		return -EINVAL;
+
+	se->dvalues = kmalloc(ec->max * sizeof(u32), GFP_KERNEL);
+	if (!se->dvalues)
+		return -ENOMEM;
+
+	memcpy(se->dvalues, ec->values, ec->max * sizeof(u32));
+	return 0;
+}
+
+static int soc_fw_denum_create(struct soc_fw *sfw, unsigned int count,
+	size_t size)
+{
+	struct snd_soc_fw_enum_control *ec;
+	struct soc_enum *se;
+	struct snd_kcontrol_new kc;
+	int i, ret, err, ext;
+
+	if (soc_fw_check_control_count(sfw,
+		sizeof(struct snd_soc_fw_enum_control), count, size)) {
+		dev_err(sfw->dev, "ASoC: invalid count %d for enum controls\n",
+			count);
+		return -EINVAL;
+	}
+
+	for (i = 0; i < count; i++) {
+		ec = (struct snd_soc_fw_enum_control*)sfw->pos;
+		sfw->pos += sizeof(struct snd_soc_fw_enum_control);
+
+		/* validate kcontrol */
+		if (strnlen(ec->hdr.name, SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE)
+			return -EINVAL;
+
+		se = kzalloc(sizeof(*se), GFP_KERNEL);
+		if (!se)
+			return -ENOMEM;
+
+		dev_dbg(sfw->dev, "ASoC: adding enum kcontrol %s size %d\n",
+			ec->hdr.name, ec->max);
+
+		memset(&kc, 0, sizeof(kc));
+		kc.name = ec->hdr.name;
+		kc.private_value = (long)se;
+		kc.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+		kc.access = ec->hdr.access;
+
+		se->reg = ec->reg;
+		se->reg2 = ec->reg2;
+		se->shift_l = ec->shift_l;
+		se->shift_r = ec->shift_r;
+		se->max = ec->max;
+		se->mask = ec->mask;
+		INIT_LIST_HEAD(&se->list);
+
+		switch (SOC_CONTROL_GET_ID_INFO(ec->hdr.index)) {
+		case SOC_CONTROL_TYPE_ENUM:
+		case SOC_CONTROL_TYPE_ENUM_EXT:
+		case SOC_DAPM_TYPE_ENUM_EXT:
+		case SOC_DAPM_TYPE_ENUM_DOUBLE:
+		case SOC_DAPM_TYPE_ENUM_VIRT:
+			err = soc_fw_denum_create_texts(se, ec);
+			if (err < 0) {
+				dev_err(sfw->dev, "ASoC: could not create"
+					" texts for %s\n", ec->hdr.name);
+				kfree(se);
+				continue;
+			}
+			break;
+		case SOC_DAPM_TYPE_ENUM_VALUE:
+		case SOC_CONTROL_TYPE_ENUM_VALUE:
+			err = soc_fw_denum_create_values(se, ec);
+			if (err < 0) {
+				dev_err(sfw->dev, "ASoC: could not create"
+					" values for %s\n", ec->hdr.name);
+				kfree(se);
+				continue;
+			}
+			break;
+		default:
+			dev_err(sfw->dev, "ASoC: invalid enum control type %d"
+				" for %s\n", ec->hdr.index, ec->hdr.name);
+			kfree(se);
+			continue;
+		}
+
+		/* map standard io handlers and check for external handlers */
+		ext = soc_fw_kcontrol_bind_io(ec->hdr.index, &kc, io_ops,
+			ARRAY_SIZE(io_ops));
+		if (ext) {
+			/* none exist, so now try and map ext handlers */
+			ext = soc_fw_kcontrol_bind_io(ec->hdr.index, &kc,
+				sfw->io_ops, sfw->io_ops_count);
+			if (ext) {
+				dev_err(sfw->dev, "ASoC: no complete enum IO handler"
+					" for %s type (g,p,i) %d:%d:%d\n",
+					ec->hdr.name,
+					SOC_CONTROL_GET_ID_GET(ec->hdr.index),
+					SOC_CONTROL_GET_ID_PUT(ec->hdr.index),
+					SOC_CONTROL_GET_ID_INFO(ec->hdr.index));
+				kfree(se);
+				continue;
+			}
+
+			err = soc_fw_init_kcontrol(sfw, &kc);
+			if (err < 0) {
+				dev_err(sfw->dev, "ASoC: failed to init %s\n",
+					ec->hdr.name);
+				kfree(se);
+				continue;
+			}
+		}
+
+		/* register control here */
+		ret = soc_fw_add_kcontrol(sfw, &kc, &se->dcontrol);
+		if (ret < 0) {
+			dev_err(sfw->dev, "ASoC: could not add kcontrol %s\n",
+				ec->hdr.name);
+			kfree(se);
+			continue;
+		}
+
+		soc_fw_list_add_enum(sfw, se);
+	}
+
+	return 0;
+}
+
+static int soc_fw_kcontrol_load(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	struct snd_soc_fw_kcontrol *sfwk =
+		(struct snd_soc_fw_kcontrol*)sfw->pos;
+	struct snd_soc_fw_control_hdr *control_hdr;
+	int i;
+
+	if (sfw->pass != SOC_FW_PASS_MIXER) {
+		sfw->pos += sizeof(struct snd_soc_fw_kcontrol) + hdr->size;
+		return 0;
+	}
+
+	sfw->pos += sizeof(struct snd_soc_fw_kcontrol);
+	control_hdr = (struct snd_soc_fw_control_hdr*)sfw->pos;
+
+	dev_dbg(sfw->dev, "ASoC: adding %d kcontrols\n", sfwk->count);
+
+	for (i = 0; i < sfwk->count; i++) {
+		switch (SOC_CONTROL_GET_ID_INFO(control_hdr->index)) {
+		case SOC_CONTROL_TYPE_VOLSW:
+		case SOC_CONTROL_TYPE_STROBE:
+		case SOC_CONTROL_TYPE_VOLSW_SX:
+		case SOC_CONTROL_TYPE_VOLSW_S8:
+		case SOC_CONTROL_TYPE_VOLSW_XR_SX:
+		case SOC_CONTROL_TYPE_BYTES:
+		case SOC_CONTROL_TYPE_BOOL_EXT:
+		case SOC_CONTROL_TYPE_RANGE:
+		case SOC_DAPM_TYPE_VOLSW:
+		case SOC_DAPM_TYPE_PIN:
+			soc_fw_dmixer_create(sfw, 1, hdr->size);
+			break;
+		case SOC_CONTROL_TYPE_ENUM:
+		case SOC_CONTROL_TYPE_ENUM_EXT:
+		case SOC_CONTROL_TYPE_ENUM_VALUE:
+		case SOC_DAPM_TYPE_ENUM_DOUBLE:
+		case SOC_DAPM_TYPE_ENUM_VIRT:
+		case SOC_DAPM_TYPE_ENUM_VALUE:
+		case SOC_DAPM_TYPE_ENUM_EXT:
+			soc_fw_denum_create(sfw, 1, hdr->size);
+			break;
+		default:
+			dev_err(sfw->dev, "ASoC: invalid control type %d:%d:%d count %d\n",
+				SOC_CONTROL_GET_ID_GET(control_hdr->index),
+				SOC_CONTROL_GET_ID_PUT(control_hdr->index),
+				SOC_CONTROL_GET_ID_INFO(control_hdr->index),
+				sfwk->count);
+		}
+	}
+	return 0;
+}
+
+static int soc_fw_dapm_graph_load(struct soc_fw *sfw,
+	struct snd_soc_fw_hdr *hdr)
+{
+	struct snd_soc_dapm_context *dapm = soc_fw_dapm_get(sfw);
+	struct snd_soc_dapm_route route;
+	struct snd_soc_fw_dapm_elems *elem_info =
+		(struct snd_soc_fw_dapm_elems*)sfw->pos;
+	struct snd_soc_fw_dapm_graph_elem *elem;
+	int count = elem_info->count, i;
+
+	if (sfw->pass != SOC_FW_PASS_GRAPH) {
+		sfw->pos += sizeof(struct snd_soc_fw_dapm_elems) + hdr->size;
+		return 0;
+	}
+
+	sfw->pos += sizeof(struct snd_soc_fw_dapm_elems);
+
+	if (soc_fw_check_control_count(sfw,
+		sizeof(struct snd_soc_fw_dapm_graph_elem), count, hdr->size)) {
+		dev_err(sfw->dev, "ASoC: invalid count %d for DAPM routes\n",
+			count);
+		return -EINVAL;
+	}
+
+	dev_dbg(sfw->dev, "ASoC: adding %d DAPM routes\n", count);
+
+	for (i = 0; i < count; i++) {
+		elem = (struct snd_soc_fw_dapm_graph_elem *)sfw->pos;
+		sfw->pos += sizeof(struct snd_soc_fw_dapm_graph_elem);
+
+		/* validate routes */
+		if (strnlen(elem->source, SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE)
+			return -EINVAL;
+		if (strnlen(elem->sink, SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE)
+			return -EINVAL;
+		if (strnlen(elem->control, SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE)
+			return -EINVAL;
+
+		route.source = elem->source;
+		route.sink = elem->sink;
+		if (strnlen(elem->control, SND_SOC_FW_TEXT_SIZE) == 0)
+			route.control = NULL;
+		else
+			route.control = elem->control;
+
+		/* add route, but keep going if some fail */
+		snd_soc_dapm_add_routes(dapm, &route, 1);
+	}
+
+	return 0;
+}
+
+static struct snd_kcontrol_new *soc_fw_dapm_widget_dmixer_create(struct soc_fw *sfw,
+	int num_kcontrols)
+{
+	struct snd_kcontrol_new *kc;
+	struct soc_mixer_control *sm;
+	struct snd_soc_fw_mixer_control *mc;
+	int i, err, ext;
+
+	kc = kzalloc(sizeof(*kc) * num_kcontrols, GFP_KERNEL);
+	if (!kc)
+		return NULL;
+
+	for (i = 0; i < num_kcontrols; i++) {
+		sm = kzalloc(sizeof(*sm), GFP_KERNEL);
+		if (!sm)
+			goto err;
+
+		mc = (struct snd_soc_fw_mixer_control*)sfw->pos;
+		sfw->pos += sizeof(struct snd_soc_fw_mixer_control);
+
+		/* validate kcontrol */
+		if (strnlen(mc->hdr.name, SND_SOC_FW_TEXT_SIZE) ==
+			SND_SOC_FW_TEXT_SIZE)
+			goto err_str;
+
+		dev_dbg(sfw->dev, " adding DAPM widget mixer control %s at %d\n",
+			mc->hdr.name, i);
+
+		kc[i].name = mc->hdr.name;
+		kc[i].private_value = (long)sm;
+		kc[i].iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+		kc[i].access = mc->hdr.access;
+
+		sm->reg = mc->reg;
+		sm->rreg = mc->rreg;
+		sm->shift = mc->shift;
+		sm->rshift = mc->rshift;
+		sm->max = mc->max;
+		sm->min = mc->min;
+		sm->invert = mc->invert;
+		sm->platform_max = mc->platform_max;
+		INIT_LIST_HEAD(&sm->list);
+
+		/* map standard io handlers and check for external handlers */
+		ext = soc_fw_kcontrol_bind_io(mc->hdr.index, &kc[i], io_ops,
+			ARRAY_SIZE(io_ops));
+		if (ext) {
+			/* none exist, so now try and map ext handlers */
+			ext = soc_fw_kcontrol_bind_io(mc->hdr.index, &kc[i],
+				sfw->io_ops, sfw->io_ops_count);
+			if (ext) {
+				dev_err(sfw->dev, "ASoC: no complete widget mixer IO handler"
+					" for %s type (g,p,i) %d:%d:%d\n",
+					mc->hdr.name,
+					SOC_CONTROL_GET_ID_GET(mc->hdr.index),
+					SOC_CONTROL_GET_ID_PUT(mc->hdr.index),
+					SOC_CONTROL_GET_ID_INFO(mc->hdr.index));
+				kfree(sm);
+				continue;
+			}
+
+			err = soc_fw_init_kcontrol(sfw, &kc[i]);
+			if (err < 0) {
+				dev_err(sfw->dev, "ASoC: failed to init %s\n",
+					mc->hdr.name);
+				kfree(sm);
+				continue;
+			}
+		}
+	}
+	return kc;
+err_str:
+	kfree(sm);
+err:
+	for (--i; i >= 0; i--)
+		kfree((void*)kc[i].private_value);
+	kfree(kc);
+	return NULL;
+}
+
+static struct snd_kcontrol_new *soc_fw_dapm_widget_denum_create(struct soc_fw *sfw)
+{
+	struct snd_kcontrol_new *kc;
+	struct snd_soc_fw_enum_control *ec;
+	struct soc_enum *se;
+	int i, err, ext;
+
+	ec = (struct snd_soc_fw_enum_control*)sfw->pos;
+	sfw->pos += sizeof(struct snd_soc_fw_enum_control);
+
+	/* validate kcontrol */
+	if (strnlen(ec->hdr.name, SND_SOC_FW_TEXT_SIZE) ==
+		SND_SOC_FW_TEXT_SIZE)
+		return NULL;
+
+	kc = kzalloc(sizeof(*kc), GFP_KERNEL);
+	if (!kc)
+		return NULL;
+
+	se = kzalloc(sizeof(*se), GFP_KERNEL);
+	if (!se)
+		goto err_se;
+
+	dev_dbg(sfw->dev, " adding DAPM widget enum control %s\n",
+		ec->hdr.name);
+
+	kc->name = ec->hdr.name;
+	kc->private_value = (long)se;
+	kc->iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+	kc->access = ec->hdr.access;
+
+	se->reg = ec->reg;
+	se->reg2 = ec->reg2;
+	se->shift_l = ec->shift_l;
+	se->shift_r = ec->shift_r;
+	se->max = ec->max;
+	se->mask = ec->mask;
+
+	switch (SOC_CONTROL_GET_ID_INFO(ec->hdr.index)) {
+	case SOC_CONTROL_TYPE_ENUM:
+	case SOC_CONTROL_TYPE_ENUM_EXT:
+	case SOC_DAPM_TYPE_ENUM_EXT:
+	case SOC_DAPM_TYPE_ENUM_DOUBLE:
+	case SOC_DAPM_TYPE_ENUM_VIRT:
+		err = soc_fw_denum_create_texts(se, ec);
+		if (err < 0) {
+			dev_err(sfw->dev, "ASoC: could not create"
+				" texts for %s\n", ec->hdr.name);
+			goto err_se;
+		}
+		break;
+	case SOC_CONTROL_TYPE_ENUM_VALUE:
+	case SOC_DAPM_TYPE_ENUM_VALUE:
+		err = soc_fw_denum_create_values(se, ec);
+		if (err < 0) {
+			dev_err(sfw->dev, "ASoC: could not create"
+				" values for %s\n", ec->hdr.name);
+			goto err_se;
+		}
+		break;
+	default:
+		dev_err(sfw->dev, "ASoC: invalid enum control type %d for %s\n",
+			ec->hdr.index, ec->hdr.name);
+		goto err_se;
+	}
+
+	/* map standard io handlers and check for external handlers */
+	ext = soc_fw_kcontrol_bind_io(ec->hdr.index, kc, io_ops,
+		ARRAY_SIZE(io_ops));
+	if (ext) {
+		/* none exist, so now try and map ext handlers */
+		ext = soc_fw_kcontrol_bind_io(ec->hdr.index, kc,
+			sfw->io_ops, sfw->io_ops_count);
+		if (ext) {
+			dev_err(sfw->dev, "ASoC: no complete widget enum IO handler"
+				" for %s type (g,p,i) %d:%d:%d\n",
+				ec->hdr.name,
+				SOC_CONTROL_GET_ID_GET(ec->hdr.index),
+				SOC_CONTROL_GET_ID_PUT(ec->hdr.index),
+				SOC_CONTROL_GET_ID_INFO(ec->hdr.index));
+			goto err_se;
+		}
+
+		err = soc_fw_init_kcontrol(sfw, kc);
+		if (err < 0) {
+			dev_err(sfw->dev, "ASoC: failed to init %s\n",
+				ec->hdr.name);
+			goto err_se;
+		}
+	}
+	return kc;
+
+err_se:
+	kfree(kc);
+
+	/* free texts */
+	if (se->dvalues)
+		kfree(se->dvalues);
+	else {
+		for (i = 0; i < ec->max; i++)
+			kfree(se->dtexts[i]);
+	}
+	kfree(se);
+
+	return NULL;
+}
+
+static int soc_fw_dapm_widget_create(struct soc_fw *sfw,
+	struct snd_soc_fw_dapm_widget *w)
+{
+	struct snd_soc_dapm_context *dapm = soc_fw_dapm_get(sfw);
+	struct snd_soc_dapm_widget widget;
+	struct snd_soc_fw_control_hdr *control_hdr;
+	int ret = 0;
+
+	if (strnlen(w->name, SND_SOC_FW_TEXT_SIZE) ==
+		SND_SOC_FW_TEXT_SIZE)
+		return -EINVAL;
+	if (strnlen(w->sname, SND_SOC_FW_TEXT_SIZE) ==
+		SND_SOC_FW_TEXT_SIZE)
+		return -EINVAL;
+
+	dev_dbg(sfw->dev, "ASoC: creating DAPM widget %s id %d\n",
+		w->name, w->id);
+
+	memset(&widget, 0, sizeof(widget));
+	widget.id = w->id;
+	widget.name = kstrdup(w->name, GFP_KERNEL);
+	if (!widget.name)
+		return -ENOMEM;
+	widget.sname = kstrdup(w->sname, GFP_KERNEL);
+	if (!widget.sname) {
+		ret = -ENOMEM;
+		goto err;
+	}
+	widget.reg = w->reg;
+	widget.shift = w->shift;
+	widget.mask = w->mask;
+	widget.invert = w->invert;
+	widget.ignore_suspend = w->ignore_suspend;
+
+	sfw->pos += sizeof(struct snd_soc_fw_dapm_widget);
+	if (w->kcontrol.count == 0) {
+		widget.num_kcontrols = 0;
+		goto widget;
+	}
+
+	control_hdr = (struct snd_soc_fw_control_hdr*)sfw->pos;
+	dev_dbg(sfw->dev, "ASoC: widget %s has %d controls of type %x\n",
+		w->name, w->kcontrol.count, control_hdr->index);
+
+	switch (SOC_CONTROL_GET_ID_INFO(control_hdr->index)) {
+	case SOC_CONTROL_TYPE_VOLSW:
+	case SOC_CONTROL_TYPE_STROBE:
+	case SOC_CONTROL_TYPE_VOLSW_SX:
+	case SOC_CONTROL_TYPE_VOLSW_S8:
+	case SOC_CONTROL_TYPE_VOLSW_XR_SX:
+	case SOC_CONTROL_TYPE_BYTES:
+	case SOC_CONTROL_TYPE_BOOL_EXT:
+	case SOC_CONTROL_TYPE_RANGE:
+	case SOC_DAPM_TYPE_VOLSW:
+		widget.num_kcontrols = widget.dmixer = w->kcontrol.count;
+		widget.kcontrol_news = soc_fw_dapm_widget_dmixer_create(sfw,
+			widget.num_kcontrols);
+		if (!widget.kcontrol_news) {
+			ret = -ENOMEM;
+			goto hdr_err;
+		}
+		ret = soc_fw_widget_load(sfw, &widget);
+		if (ret < 0)
+			goto hdr_err;
+		break;
+	case SOC_CONTROL_TYPE_ENUM:
+	case SOC_CONTROL_TYPE_ENUM_EXT:
+	case SOC_CONTROL_TYPE_ENUM_VALUE:
+	case SOC_DAPM_TYPE_ENUM_DOUBLE:
+	case SOC_DAPM_TYPE_ENUM_VIRT:
+	case SOC_DAPM_TYPE_ENUM_VALUE:
+	case SOC_DAPM_TYPE_ENUM_EXT:
+		widget.num_kcontrols = widget.denum = 1;
+		widget.kcontrol_news = soc_fw_dapm_widget_denum_create(sfw);
+		if (!widget.kcontrol_news) {
+			ret = -ENOMEM;
+			goto hdr_err;
+		}
+		ret = soc_fw_widget_load(sfw, &widget);
+		if (ret < 0)
+			goto hdr_err;
+		break;
+	default:
+		dev_err(sfw->dev, "ASoC: invalid widget control type %d:%d:%d\n",
+			SOC_CONTROL_GET_ID_GET(control_hdr->index),
+			SOC_CONTROL_GET_ID_PUT(control_hdr->index),
+			SOC_CONTROL_GET_ID_INFO(control_hdr->index));
+		ret = -EINVAL;
+		goto hdr_err;
+	}
+
+widget:
+	ret = snd_soc_dapm_new_controls(dapm, &widget, 1);
+	if (ret < 0) {
+		dev_err(sfw->dev, "ASoC: failed to create widget %s controls\n",
+			w->name);
+		goto hdr_err;
+	}
+
+hdr_err:
+	kfree(widget.sname);
+err:
+	kfree(widget.name);
+	return ret;
+}
+
+static int soc_fw_dapm_widget_load(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	struct snd_soc_fw_dapm_elems *elem_info =
+		(struct snd_soc_fw_dapm_elems*)sfw->pos;
+	struct snd_soc_fw_dapm_widget *widget;
+	int ret, count = elem_info->count, i;
+
+	if (sfw->pass != SOC_FW_PASS_WIDGET)
+		return 0;
+
+	sfw->pos += sizeof(struct snd_soc_fw_dapm_elems);
+
+	if (soc_fw_check_control_count(sfw,
+		sizeof(struct snd_soc_fw_dapm_graph_elem), count, hdr->size)) {
+		dev_err(sfw->dev, "ASoC: invalid count %d for widgets\n", count);
+		return -EINVAL;
+	}
+
+	dev_dbg(sfw->dev, "ASoC: adding %d DAPM widgets\n", count);
+
+	for (i = 0; i < count; i++) {
+		widget = (struct snd_soc_fw_dapm_widget*) sfw->pos;
+		ret = soc_fw_dapm_widget_create(sfw, widget);
+		if (ret < 0)
+			dev_err(sfw->dev, "ASoC: failed to load widget %s\n",
+				widget->name);
+	}
+
+	return 0;
+}
+
+static int soc_fw_dapm_complete(struct soc_fw *sfw)
+{
+	struct snd_soc_dapm_context *dapm = soc_fw_dapm_get(sfw);
+	int ret;
+
+	ret = snd_soc_dapm_new_widgets(dapm);
+	if (ret < 0)
+		dev_err(sfw->dev, "ASoC: failed to create new widgets %d\n",
+			ret);
+
+	return ret;
+}
+
+/* Coefficients with mixer header */
+static int soc_fw_coeff_load(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	struct snd_soc_fw_kcontrol *sfwk =
+		(struct snd_soc_fw_kcontrol*)sfw->pos;
+	struct snd_soc_fw_control_hdr *control_hdr;
+	struct snd_soc_fw_hdr *vhdr;
+	int ret;
+
+	if (sfw->pass != SOC_FW_PASS_COEFF)
+		return 0;
+
+	/* vendor coefficient data is encapsulated with hdrs in generic
+	  coefficient controls */
+	if (hdr->vendor_type != 0)
+		return 0;
+
+	dev_dbg(sfw->dev, "ASoC: got %d new coefficients\n", sfwk->count);
+
+	sfw->pos += sizeof(struct snd_soc_fw_kcontrol);
+	control_hdr = (struct snd_soc_fw_control_hdr*)sfw->pos;
+
+	switch (SOC_CONTROL_GET_ID_INFO(control_hdr->index)) {
+	case SOC_CONTROL_TYPE_ENUM:
+	case SOC_CONTROL_TYPE_ENUM_EXT:
+	case SOC_CONTROL_TYPE_ENUM_VALUE:
+		ret = soc_fw_denum_create(sfw, 1, hdr->size);
+		if (ret < 0) {
+			dev_err(sfw->dev, "ASoC: failed to create coeff enum %d\n",
+				ret);
+			return ret;
+		}
+		break;
+	default:
+		dev_err(sfw->dev, "ASoC: invalid coeff control type %d count %d\n",
+			SOC_CONTROL_GET_ID_INFO(control_hdr->index),
+			sfwk->count);
+		return -EINVAL;
+	}
+
+	vhdr = (struct snd_soc_fw_hdr *)sfw->pos;
+
+	ret = soc_fw_vendor_load_(sfw, vhdr);
+	if (ret < 0) {
+		dev_err(sfw->dev, "ASoC: unabled to load coeff data %d\n", ret);
+		return ret;
+	}
+	sfw->pos += sizeof(*vhdr) + vhdr->size;
+	vhdr = (struct snd_soc_fw_hdr *)sfw->pos;
+
+	return 0;
+}
+
+static int soc_fw_dapm_pin_load(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	/* TODO: add static enabled/disabled pins */
+	dev_err(sfw->dev, "ASoC: Firmware pins not supported\n");
+	return 0;
+}
+
+static int soc_fw_dai_link_load(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	/* TODO: add DAI links based on FW routing between components */
+	dev_err(sfw->dev, "ASoC: Firmware DAIs not supported\n");
+	return 0;
+}
+
+static int soc_valid_header(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	if (soc_fw_get_hdr_offset(sfw) >= sfw->fw->size)
+		return 0;
+
+	if (hdr->magic != SND_SOC_FW_MAGIC) {
+		dev_err(sfw->dev, "ASoC: %s at pass %d does not have a valid"
+			" header got %x at offset 0x%x size 0x%x.\n",
+			sfw->file, sfw->pass, hdr->magic,
+			soc_fw_get_hdr_offset(sfw), sfw->fw->size);
+		return -EINVAL;
+	}
+
+	if (hdr->size == 0) {
+		dev_err(sfw->dev, "ASoC: %s header has 0 size at offset 0x%x.\n",
+			sfw->file, soc_fw_get_hdr_offset(sfw));
+		return -EINVAL;
+	}
+
+	if (sfw->pass == hdr->type)
+		dev_dbg(sfw->dev, "ASoC: Got 0x%x bytes of type %d version %d"
+			" vendor %d at pass %d\n", hdr->size, hdr->type,
+			hdr->version, hdr->vendor_type, sfw->pass);
+
+	return 1;
+}
+
+static int soc_fw_load_header(struct soc_fw *sfw, struct snd_soc_fw_hdr *hdr)
+{
+	sfw->pos = sfw->hdr_pos + sizeof(struct snd_soc_fw_hdr);
+
+	switch (hdr->type) {
+	case SND_SOC_FW_MIXER:
+		return soc_fw_kcontrol_load(sfw, hdr);
+	case SND_SOC_FW_DAPM_GRAPH:
+		return soc_fw_dapm_graph_load(sfw, hdr);
+	case SND_SOC_FW_DAPM_PINS:
+		return soc_fw_dapm_pin_load(sfw, hdr);
+	case SND_SOC_FW_DAPM_WIDGET:
+		return soc_fw_dapm_widget_load(sfw, hdr);
+	case SND_SOC_FW_DAI_LINK:
+		return soc_fw_dai_link_load(sfw, hdr);
+	case SND_SOC_FW_COEFF:
+		return soc_fw_coeff_load(sfw, hdr);
+	default:
+		return soc_fw_vendor_load(sfw, hdr);
+	}
+
+	return 0;
+}
+
+static int soc_fw_process_headers(struct soc_fw *sfw)
+{
+	struct snd_soc_fw_hdr *hdr;
+	int ret;
+
+	sfw->pass = SOC_FW_PASS_START;
+
+	while (sfw->pass <= SOC_FW_PASS_END) {
+
+		sfw->hdr_pos = sfw->fw->data;
+		hdr = (struct snd_soc_fw_hdr *)sfw->hdr_pos;
+
+		while (!soc_fw_is_eof(sfw)) {
+
+			ret = soc_valid_header(sfw, hdr);
+			if (ret < 0)
+				return ret;
+			else if (ret == 0)
+				break;
+
+			ret = soc_fw_load_header(sfw, hdr);
+			if (ret < 0)
+				return ret;
+
+			sfw->hdr_pos += hdr->size + sizeof(struct snd_soc_fw_hdr);
+			hdr = (struct snd_soc_fw_hdr *)sfw->hdr_pos;
+		}
+		sfw->pass++;
+	}
+
+	ret = soc_fw_dapm_complete(sfw);
+	if (ret < 0)
+		dev_err(sfw->dev, "ASoC: failed to initialise DAPM from Firmware\n");
+
+	return ret;
+}
+
+static int soc_fw_load(struct soc_fw *sfw)
+{
+	int ret;
+
+	ret = soc_fw_process_headers(sfw);
+	if (ret == 0)
+		soc_fw_complete(sfw);
+
+	return ret;
+}
+
+int snd_soc_fw_load_codec(struct snd_soc_codec *codec,
+	struct snd_soc_fw_codec_ops *ops, const struct firmware *fw)
+{
+	struct soc_fw sfw;
+
+	memset(&sfw, 0, sizeof(sfw));
+
+	sfw.fw = fw;
+	sfw.dev = codec->dev;
+	sfw.codec = codec;
+	sfw.codec_ops = ops;
+	sfw.io_ops = ops->io_ops;
+	sfw.io_ops_count = ops->io_ops_count;
+
+	return soc_fw_load(&sfw);
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_load_codec);
+
+int snd_soc_fw_load_platform(struct snd_soc_platform *platform,
+	struct snd_soc_fw_platform_ops *ops, const struct firmware *fw)
+{
+	struct soc_fw sfw;
+
+	memset(&sfw, 0, sizeof(sfw));
+
+	sfw.fw = fw;
+	sfw.dev = platform->dev;
+	sfw.platform = platform;
+	sfw.platform_ops = ops;
+	sfw.io_ops = ops->io_ops;
+	sfw.io_ops_count = ops->io_ops_count;
+
+	return soc_fw_load(&sfw);
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_load_platform);
+
+int snd_soc_fw_load_card(struct snd_soc_card *card,
+	struct snd_soc_fw_card_ops *ops, const struct firmware *fw)
+{
+	struct soc_fw sfw;
+
+	memset(&sfw, 0, sizeof(sfw));
+
+	sfw.fw = fw;
+	sfw.dev = card->dev;
+	sfw.card = card;
+	sfw.card_ops = ops;
+	sfw.io_ops = ops->io_ops;
+	sfw.io_ops_count = ops->io_ops_count;
+
+	return soc_fw_load(&sfw);
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_load_card);
+
+/* remove this dynamic widget */
+void snd_soc_fw_dcontrols_remove_widget(struct snd_soc_dapm_widget *w)
+{
+	struct snd_card *card = w->dapm->card->snd_card;
+	int i;
+
+	/*
+	 * Dynamic Widgets either have 1 enum kcontrol or 1..N mixers.
+	 * The enumm may either have an array of values or strings.
+	 */
+	if (w->denum) {
+		struct soc_enum *se =
+			(struct soc_enum *)w->kcontrols[0]->private_value;
+
+		snd_ctl_remove(card, w->kcontrols[0]);
+		if (se->dvalues)
+			kfree(se->dvalues);
+		else {
+			for (i = 0; i < se->max; i++)
+				kfree(se->dtexts[i]);
+		}
+
+		kfree(se);
+	} else if (w->dmixer) {
+		for (i = 0; i < w->num_kcontrols; i++) {
+			struct snd_kcontrol *kcontrol = w->kcontrols[i];
+			struct soc_mixer_control *sm =
+			(struct soc_mixer_control *) kcontrol->private_value;
+
+			if (w->kcontrols[i]->tlv.p)
+				kfree(w->kcontrols[i]->tlv.p);
+
+			snd_ctl_remove(card, w->kcontrols[i]);
+			kfree(sm);
+		}
+	}
+	kfree(w->kcontrol_news);
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_dcontrols_remove_widget);
+
+/* remove all dynamic widgets from this context */
+void snd_soc_fw_dcontrols_remove_widgets(struct snd_soc_dapm_context *dapm)
+{
+	struct snd_soc_dapm_widget *w, *next_w;
+	struct snd_soc_dapm_path *p, *next_p;
+
+	list_for_each_entry_safe(w, next_w, &dapm->card->widgets, list) {
+		if (!w->dmixer && !w->denum && w->dapm != dapm)
+			continue;
+		list_del(&w->list);
+		/*
+		 * remove source and sink paths associated to this widget.
+		 * While removing the path, remove reference to it from both
+		 * source and sink widgets so that path is removed only once.
+		 */
+		list_for_each_entry_safe(p, next_p, &w->sources, list_sink) {
+			list_del(&p->list_sink);
+			list_del(&p->list_source);
+			list_del(&p->list);
+			kfree(p->long_name);
+			kfree(p);
+		}
+		list_for_each_entry_safe(p, next_p, &w->sinks, list_source) {
+			list_del(&p->list_sink);
+			list_del(&p->list_source);
+			list_del(&p->list);
+			kfree(p->long_name);
+			kfree(p);
+		}
+		/* check and free and dynamic widget kcontrols */
+		snd_soc_fw_dcontrols_remove_widget(w);
+		kfree(w->kcontrols);
+		kfree(w->name);
+		kfree(w);
+	}
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_dcontrols_remove_widgets);
+
+/* remove dynamic controls from the codec driver only */
+void snd_soc_fw_dcontrols_remove_codec(struct snd_soc_codec *codec)
+{
+	struct soc_mixer_control *sm, *next_sm;
+	struct soc_enum *se, *next_se;
+	struct snd_card *card = codec->card->snd_card;
+	const unsigned int *p = NULL;
+	int i;
+
+	list_for_each_entry_safe(sm, next_sm, &codec->dmixers, list) {
+
+		if (sm->dcontrol->tlv.p)
+			p = sm->dcontrol->tlv.p;
+		snd_ctl_remove(card, sm->dcontrol);
+		list_del(&sm->list);
+		kfree(sm);
+		kfree(p);
+	}
+
+	list_for_each_entry_safe(se, next_se, &codec->denums, list) {
+
+		snd_ctl_remove(card, se->dcontrol);
+		list_del(&se->list);
+		if (se->dvalues)
+			kfree(se->dvalues);
+		else {
+			for (i = 0; i < se->max; i++)
+				kfree(se->dtexts[i]);
+		}
+		kfree(se);
+	}
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_dcontrols_remove_codec);
+
+/* remove dynamic controls from the platform driver only */
+void snd_soc_fw_dcontrols_remove_platform(struct snd_soc_platform *platform)
+{
+	struct soc_mixer_control *sm, *next_sm;
+	struct soc_enum *se, *next_se;
+	struct snd_card *card = platform->card->snd_card;
+	const unsigned int *p = NULL;
+	int i;
+
+	list_for_each_entry_safe(sm, next_sm, &platform->dmixers, list) {
+
+		if (sm->dcontrol->tlv.p)
+			p = sm->dcontrol->tlv.p;
+		snd_ctl_remove(card, sm->dcontrol);
+		list_del(&sm->list);
+		kfree(sm);
+		kfree(p);
+	}
+
+	list_for_each_entry_safe(se, next_se, &platform->denums, list) {
+
+		snd_ctl_remove(card, se->dcontrol);
+		list_del(&se->list);
+		if (se->dvalues)
+			kfree(se->dvalues);
+		else {
+			for (i = 0; i < se->max; i++)
+				kfree(se->dtexts[i]);
+		}
+		kfree(se);
+	}
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_dcontrols_remove_platform);
+
+/* remove dynamic controls from the card driver only */
+void snd_soc_fw_dcontrols_remove_card(struct snd_soc_card *soc_card)
+{
+	struct soc_mixer_control *sm, *next_sm;
+	struct soc_enum *se, *next_se;
+	struct snd_card *card = soc_card->snd_card;
+	const unsigned int *p = NULL;
+	int i;
+
+	list_for_each_entry_safe(sm, next_sm, &soc_card->dmixers, list) {
+
+		if (sm->dcontrol->tlv.p)
+			p = sm->dcontrol->tlv.p;
+		snd_ctl_remove(card, sm->dcontrol);
+		list_del(&sm->list);
+		kfree(sm);
+		kfree(p);
+	}
+
+	list_for_each_entry_safe(se, next_se, &soc_card->denums, list) {
+
+		snd_ctl_remove(card, se->dcontrol);
+		list_del(&se->list);
+		if (se->dvalues)
+			kfree(se->dvalues);
+		else {
+			for (i = 0; i < se->max; i++)
+				kfree(se->dtexts[i]);
+		}
+		kfree(se);
+	}
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_dcontrols_remove_card);
+
+/* remove all dynamic controls from sound card and components */
+int snd_soc_fw_dcontrols_remove_all(struct snd_soc_card *card)
+{
+	struct snd_soc_codec *codec;
+	struct snd_soc_platform *platform;
+
+	list_for_each_entry(codec, &card->codec_dev_list, card_list)
+		snd_soc_fw_dcontrols_remove_codec(codec);
+
+	list_for_each_entry(platform, &card->platform_dev_list, card_list)
+		snd_soc_fw_dcontrols_remove_platform(platform);
+
+	snd_soc_fw_dcontrols_remove_card(card);
+	return 0;
+}
+EXPORT_SYMBOL_GPL(snd_soc_fw_dcontrols_remove_all);
-- 
1.7.10.4



More information about the Alsa-devel mailing list