ALSA: jack: implement software jack injection via debugfs

This change adds audio jack injection feature through debugfs, with
this feature, we could validate alsa userspace changes by injecting
plugin or plugout events to the non-phantom audio jacks.

With this change, the sound core will build the folders
$debugfs_mount_dir/sound/cardN if SND_DEBUG and DEBUG_FS are enabled.
And if users also enable the SND_JACK_INJECTION_DEBUG, the jack
injection nodes will be built in the folder cardN like below:

$tree $debugfs_mount_dir/sound
$debugfs_mount_dir/sound
├── card0
│   ├── HDMI_DP_pcm_10_Jack
│   │   ├── jackin_inject
│   │   ├── kctl_id
│   │   ├── mask_bits
│   │   ├── status
│   │   ├── sw_inject_enable
│   │   └── type
...
│   └── HDMI_DP_pcm_9_Jack
│       ├── jackin_inject
│       ├── kctl_id
│       ├── mask_bits
│       ├── status
│       ├── sw_inject_enable
│       └── type
└── card1
    ├── HDMI_DP_pcm_5_Jack
    │   ├── jackin_inject
    │   ├── kctl_id
    │   ├── mask_bits
    │   ├── status
    │   ├── sw_inject_enable
    │   └── type
    ...
    ├── Headphone_Jack
    │   ├── jackin_inject
    │   ├── kctl_id
    │   ├── mask_bits
    │   ├── status
    │   ├── sw_inject_enable
    │   └── type
    └── Headset_Mic_Jack
        ├── jackin_inject
        ├── kctl_id
        ├── mask_bits
        ├── status
        ├── sw_inject_enable
        └── type

The nodes kctl_id, mask_bits, status and type are read-only, users
could check jack or jack_kctl's information through them.

The nodes sw_inject_enable and jackin_inject are directly used for
injection. The sw_inject_enable is read-write, users could check if
software injection is enabled or not on this jack, and users could
echo 1 or 0 to enable or disable software injection on this jack. Once
the injection is enabled, the jack will not change by hardware events
anymore, once the injection is disabled, the jack will restore the
last reported hardware events to the jack. The jackin_inject is
write-only, if the injection is enabled, users could echo 1 or 0 to
this node to inject plugin or plugout events to this jack.

For the detailed usage information on these nodes, please refer to
Documentation/sound/designs/jack-injection.rst.

Reviewed-by: Takashi Iwai <tiwai@suse.de>
Reviewed-by: Jaroslav Kysela <perex@perex.cz>
Reviewed-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
Signed-off-by: Hui Wang <hui.wang@canonical.com>
Link: https://lore.kernel.org/r/20210127085639.74954-2-hui.wang@canonical.com
Signed-off-by: Takashi Iwai <tiwai@suse.de>
This commit is contained in:
Hui Wang 2021-01-27 16:56:39 +08:00 committed by Takashi Iwai
parent da2a040ee7
commit 2d670ea2bd
8 changed files with 512 additions and 4 deletions

View File

@ -14,3 +14,4 @@ Designs and Implementations
powersave
oss-emulation
seq-oss
jack-injection

View File

@ -0,0 +1,166 @@
============================
ALSA Jack Software Injection
============================
Simple Introduction On Jack Injection
=====================================
Here jack injection means users could inject plugin or plugout events
to the audio jacks through debugfs interface, it is helpful to
validate ALSA userspace changes. For example, we change the audio
profile switching code in the pulseaudio, and we want to verify if the
change works as expected and if the change introduce the regression,
in this case, we could inject plugin or plugout events to an audio
jack or to some audio jacks, we don't need to physically access the
machine and plug/unplug physical devices to the audio jack.
In this design, an audio jack doesn't equal to a physical audio jack.
Sometimes a physical audio jack contains multi functions, and the
ALSA driver creates multi ``jack_kctl`` for a ``snd_jack``, here the
``snd_jack`` represents a physical audio jack and the ``jack_kctl``
represents a function, for example a physical jack has two functions:
headphone and mic_in, the ALSA ASoC driver will build 2 ``jack_kctl``
for this jack. The jack injection is implemented based on the
``jack_kctl`` instead of ``snd_jack``.
To inject events to audio jacks, we need to enable the jack injection
via ``sw_inject_enable`` first, once it is enabled, this jack will not
change the state by hardware events anymore, we could inject plugin or
plugout events via ``jackin_inject`` and check the jack state via
``status``, after we finish our test, we need to disable the jack
injection via ``sw_inject_enable`` too, once it is disabled, the jack
state will be restored according to the last reported hardware events
and will change by future hardware events.
The Layout of Jack Injection Interface
======================================
If users enable the SND_JACK_INJECTION_DEBUG in the kernel, the audio
jack injection interface will be created as below:
::
$debugfs_mount_dir/sound
|-- card0
|-- |-- HDMI_DP_pcm_10_Jack
|-- |-- |-- jackin_inject
|-- |-- |-- kctl_id
|-- |-- |-- mask_bits
|-- |-- |-- status
|-- |-- |-- sw_inject_enable
|-- |-- |-- type
...
|-- |-- HDMI_DP_pcm_9_Jack
|-- |-- jackin_inject
|-- |-- kctl_id
|-- |-- mask_bits
|-- |-- status
|-- |-- sw_inject_enable
|-- |-- type
|-- card1
|-- HDMI_DP_pcm_5_Jack
|-- |-- jackin_inject
|-- |-- kctl_id
|-- |-- mask_bits
|-- |-- status
|-- |-- sw_inject_enable
|-- |-- type
...
|-- Headphone_Jack
|-- |-- jackin_inject
|-- |-- kctl_id
|-- |-- mask_bits
|-- |-- status
|-- |-- sw_inject_enable
|-- |-- type
|-- Headset_Mic_Jack
|-- jackin_inject
|-- kctl_id
|-- mask_bits
|-- status
|-- sw_inject_enable
|-- type
The Explanation Of The Nodes
======================================
kctl_id
read-only, get jack_kctl->kctl's id
::
sound/card1/Headphone_Jack# cat kctl_id
Headphone Jack
mask_bits
read-only, get jack_kctl's supported events mask_bits
::
sound/card1/Headphone_Jack# cat mask_bits
0x0001 HEADPHONE(0x0001)
status
read-only, get jack_kctl's current status
- headphone unplugged:
::
sound/card1/Headphone_Jack# cat status
Unplugged
- headphone plugged:
::
sound/card1/Headphone_Jack# cat status
Plugged
type
read-only, get snd_jack's supported events from type (all supported events on the physical audio jack)
::
sound/card1/Headphone_Jack# cat type
0x7803 HEADPHONE(0x0001) MICROPHONE(0x0002) BTN_3(0x0800) BTN_2(0x1000) BTN_1(0x2000) BTN_0(0x4000)
sw_inject_enable
read-write, enable or disable injection
- injection disabled:
::
sound/card1/Headphone_Jack# cat sw_inject_enable
Jack: Headphone Jack Inject Enabled: 0
- injection enabled:
::
sound/card1/Headphone_Jack# cat sw_inject_enable
Jack: Headphone Jack Inject Enabled: 1
- to enable jack injection:
::
sound/card1/Headphone_Jack# echo 1 > sw_inject_enable
- to disable jack injection:
::
sound/card1/Headphone_Jack# echo 0 > sw_inject_enable
jackin_inject
write-only, inject plugin or plugout
- to inject plugin:
::
sound/card1/Headphone_Jack# echo 1 > jackin_inject
- to inject plugout:
::
sound/card1/Headphone_Jack# echo 0 > jackin_inject

View File

@ -122,6 +122,9 @@ struct snd_card {
size_t total_pcm_alloc_bytes; /* total amount of allocated buffers */
struct mutex memory_mutex; /* protection for the above */
#ifdef CONFIG_SND_DEBUG
struct dentry *debugfs_root; /* debugfs root for card */
#endif
#ifdef CONFIG_PM
unsigned int power_state; /* power state */
@ -180,6 +183,9 @@ static inline struct device *snd_card_get_device_link(struct snd_card *card)
extern int snd_major;
extern int snd_ecards_limit;
extern struct class *sound_class;
#ifdef CONFIG_SND_DEBUG
extern struct dentry *sound_debugfs_root;
#endif
void snd_request_card(int card);

View File

@ -67,6 +67,7 @@ struct snd_jack {
char name[100];
unsigned int key[6]; /* Keep in sync with definitions above */
#endif /* CONFIG_SND_JACK_INPUT_DEV */
int hw_status_cache;
void *private_data;
void (*private_free)(struct snd_jack *);
};

View File

@ -187,6 +187,15 @@ config SND_CTL_VALIDATION
from the driver are in the proper ranges or the check of the invalid
access at out-of-array areas.
config SND_JACK_INJECTION_DEBUG
bool "Sound jack injection interface via debugfs"
depends on SND_JACK && SND_DEBUG && DEBUG_FS
help
This option can be used to enable or disable sound jack
software injection.
Say Y if you are debugging via jack injection interface.
If unsure select "N".
config SND_VMASTER
bool

View File

@ -13,6 +13,7 @@
#include <linux/time.h>
#include <linux/ctype.h>
#include <linux/pm.h>
#include <linux/debugfs.h>
#include <linux/completion.h>
#include <sound/core.h>
@ -161,6 +162,9 @@ int snd_card_new(struct device *parent, int idx, const char *xid,
{
struct snd_card *card;
int err;
#ifdef CONFIG_SND_DEBUG
char name[8];
#endif
if (snd_BUG_ON(!card_ret))
return -EINVAL;
@ -244,6 +248,12 @@ int snd_card_new(struct device *parent, int idx, const char *xid,
dev_err(parent, "unable to create card info\n");
goto __error_ctl;
}
#ifdef CONFIG_SND_DEBUG
sprintf(name, "card%d", idx);
card->debugfs_root = debugfs_create_dir(name, sound_debugfs_root);
#endif
*card_ret = card;
return 0;
@ -526,6 +536,12 @@ int snd_card_free(struct snd_card *card)
return ret;
/* wait, until all devices are ready for the free operation */
wait_for_completion(&released);
#ifdef CONFIG_SND_DEBUG
debugfs_remove(card->debugfs_root);
card->debugfs_root = NULL;
#endif
return 0;
}
EXPORT_SYMBOL(snd_card_free);

View File

@ -8,6 +8,9 @@
#include <linux/input.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/ctype.h>
#include <linux/mm.h>
#include <linux/debugfs.h>
#include <sound/jack.h>
#include <sound/core.h>
#include <sound/control.h>
@ -16,6 +19,11 @@ struct snd_jack_kctl {
struct snd_kcontrol *kctl;
struct list_head list; /* list of controls belong to the same jack */
unsigned int mask_bits; /* only masked status bits are reported via kctl */
struct snd_jack *jack; /* pointer to struct snd_jack */
bool sw_inject_enable; /* allow to inject plug event via debugfs */
#ifdef CONFIG_SND_JACK_INJECTION_DEBUG
struct dentry *jack_debugfs_root; /* jack_kctl debugfs root */
#endif
};
#ifdef CONFIG_SND_JACK_INPUT_DEV
@ -109,12 +117,291 @@ static int snd_jack_dev_register(struct snd_device *device)
}
#endif /* CONFIG_SND_JACK_INPUT_DEV */
#ifdef CONFIG_SND_JACK_INJECTION_DEBUG
static void snd_jack_inject_report(struct snd_jack_kctl *jack_kctl, int status)
{
struct snd_jack *jack;
#ifdef CONFIG_SND_JACK_INPUT_DEV
int i;
#endif
if (!jack_kctl)
return;
jack = jack_kctl->jack;
if (jack_kctl->sw_inject_enable)
snd_kctl_jack_report(jack->card, jack_kctl->kctl,
status & jack_kctl->mask_bits);
#ifdef CONFIG_SND_JACK_INPUT_DEV
if (!jack->input_dev)
return;
for (i = 0; i < ARRAY_SIZE(jack->key); i++) {
int testbit = ((SND_JACK_BTN_0 >> i) & jack_kctl->mask_bits);
if (jack->type & testbit)
input_report_key(jack->input_dev, jack->key[i],
status & testbit);
}
for (i = 0; i < ARRAY_SIZE(jack_switch_types); i++) {
int testbit = ((1 << i) & jack_kctl->mask_bits);
if (jack->type & testbit)
input_report_switch(jack->input_dev,
jack_switch_types[i],
status & testbit);
}
input_sync(jack->input_dev);
#endif /* CONFIG_SND_JACK_INPUT_DEV */
}
static ssize_t sw_inject_enable_read(struct file *file,
char __user *to, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
int len, ret;
char buf[128];
len = scnprintf(buf, sizeof(buf), "%s: %s\t\t%s: %i\n", "Jack", jack_kctl->kctl->id.name,
"Inject Enabled", jack_kctl->sw_inject_enable);
ret = simple_read_from_buffer(to, count, ppos, buf, len);
return ret;
}
static ssize_t sw_inject_enable_write(struct file *file,
const char __user *from, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
int ret, err;
unsigned long enable;
char buf[8] = { 0 };
ret = simple_write_to_buffer(buf, sizeof(buf) - 1, ppos, from, count);
err = kstrtoul(buf, 0, &enable);
if (err)
return err;
if (jack_kctl->sw_inject_enable == (!!enable))
return ret;
jack_kctl->sw_inject_enable = !!enable;
if (!jack_kctl->sw_inject_enable)
snd_jack_report(jack_kctl->jack, jack_kctl->jack->hw_status_cache);
return ret;
}
static ssize_t jackin_inject_write(struct file *file,
const char __user *from, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
int ret, err;
unsigned long enable;
char buf[8] = { 0 };
if (!jack_kctl->sw_inject_enable)
return -EINVAL;
ret = simple_write_to_buffer(buf, sizeof(buf) - 1, ppos, from, count);
err = kstrtoul(buf, 0, &enable);
if (err)
return err;
snd_jack_inject_report(jack_kctl, !!enable ? jack_kctl->mask_bits : 0);
return ret;
}
static ssize_t jack_kctl_id_read(struct file *file,
char __user *to, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
char buf[64];
int len, ret;
len = scnprintf(buf, sizeof(buf), "%s\n", jack_kctl->kctl->id.name);
ret = simple_read_from_buffer(to, count, ppos, buf, len);
return ret;
}
/* the bit definition is aligned with snd_jack_types in jack.h */
static const char * const jack_events_name[] = {
"HEADPHONE(0x0001)", "MICROPHONE(0x0002)", "LINEOUT(0x0004)",
"MECHANICAL(0x0008)", "VIDEOOUT(0x0010)", "LINEIN(0x0020)",
"", "", "", "BTN_5(0x0200)", "BTN_4(0x0400)", "BTN_3(0x0800)",
"BTN_2(0x1000)", "BTN_1(0x2000)", "BTN_0(0x4000)", "",
};
/* the recommended buffer size is 256 */
static int parse_mask_bits(unsigned int mask_bits, char *buf, size_t buf_size)
{
int i;
scnprintf(buf, buf_size, "0x%04x", mask_bits);
for (i = 0; i < ARRAY_SIZE(jack_events_name); i++)
if (mask_bits & (1 << i)) {
strlcat(buf, " ", buf_size);
strlcat(buf, jack_events_name[i], buf_size);
}
strlcat(buf, "\n", buf_size);
return strlen(buf);
}
static ssize_t jack_kctl_mask_bits_read(struct file *file,
char __user *to, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
char buf[256];
int len, ret;
len = parse_mask_bits(jack_kctl->mask_bits, buf, sizeof(buf));
ret = simple_read_from_buffer(to, count, ppos, buf, len);
return ret;
}
static ssize_t jack_kctl_status_read(struct file *file,
char __user *to, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
char buf[16];
int len, ret;
len = scnprintf(buf, sizeof(buf), "%s\n", jack_kctl->kctl->private_value ?
"Plugged" : "Unplugged");
ret = simple_read_from_buffer(to, count, ppos, buf, len);
return ret;
}
#ifdef CONFIG_SND_JACK_INPUT_DEV
static ssize_t jack_type_read(struct file *file,
char __user *to, size_t count, loff_t *ppos)
{
struct snd_jack_kctl *jack_kctl = file->private_data;
char buf[256];
int len, ret;
len = parse_mask_bits(jack_kctl->jack->type, buf, sizeof(buf));
ret = simple_read_from_buffer(to, count, ppos, buf, len);
return ret;
}
static const struct file_operations jack_type_fops = {
.open = simple_open,
.read = jack_type_read,
.llseek = default_llseek,
};
#endif
static const struct file_operations sw_inject_enable_fops = {
.open = simple_open,
.read = sw_inject_enable_read,
.write = sw_inject_enable_write,
.llseek = default_llseek,
};
static const struct file_operations jackin_inject_fops = {
.open = simple_open,
.write = jackin_inject_write,
.llseek = default_llseek,
};
static const struct file_operations jack_kctl_id_fops = {
.open = simple_open,
.read = jack_kctl_id_read,
.llseek = default_llseek,
};
static const struct file_operations jack_kctl_mask_bits_fops = {
.open = simple_open,
.read = jack_kctl_mask_bits_read,
.llseek = default_llseek,
};
static const struct file_operations jack_kctl_status_fops = {
.open = simple_open,
.read = jack_kctl_status_read,
.llseek = default_llseek,
};
static int snd_jack_debugfs_add_inject_node(struct snd_jack *jack,
struct snd_jack_kctl *jack_kctl)
{
char *tname;
int i;
/* Don't create injection interface for Phantom jacks */
if (strstr(jack_kctl->kctl->id.name, "Phantom"))
return 0;
tname = kstrdup(jack_kctl->kctl->id.name, GFP_KERNEL);
if (!tname)
return -ENOMEM;
/* replace the chars which are not suitable for folder's name with _ */
for (i = 0; tname[i]; i++)
if (!isalnum(tname[i]))
tname[i] = '_';
jack_kctl->jack_debugfs_root = debugfs_create_dir(tname, jack->card->debugfs_root);
kfree(tname);
debugfs_create_file("sw_inject_enable", 0644, jack_kctl->jack_debugfs_root, jack_kctl,
&sw_inject_enable_fops);
debugfs_create_file("jackin_inject", 0200, jack_kctl->jack_debugfs_root, jack_kctl,
&jackin_inject_fops);
debugfs_create_file("kctl_id", 0444, jack_kctl->jack_debugfs_root, jack_kctl,
&jack_kctl_id_fops);
debugfs_create_file("mask_bits", 0444, jack_kctl->jack_debugfs_root, jack_kctl,
&jack_kctl_mask_bits_fops);
debugfs_create_file("status", 0444, jack_kctl->jack_debugfs_root, jack_kctl,
&jack_kctl_status_fops);
#ifdef CONFIG_SND_JACK_INPUT_DEV
debugfs_create_file("type", 0444, jack_kctl->jack_debugfs_root, jack_kctl,
&jack_type_fops);
#endif
return 0;
}
static void snd_jack_debugfs_clear_inject_node(struct snd_jack_kctl *jack_kctl)
{
debugfs_remove(jack_kctl->jack_debugfs_root);
jack_kctl->jack_debugfs_root = NULL;
}
#else /* CONFIG_SND_JACK_INJECTION_DEBUG */
static int snd_jack_debugfs_add_inject_node(struct snd_jack *jack,
struct snd_jack_kctl *jack_kctl)
{
return 0;
}
static void snd_jack_debugfs_clear_inject_node(struct snd_jack_kctl *jack_kctl)
{
}
#endif /* CONFIG_SND_JACK_INJECTION_DEBUG */
static void snd_jack_kctl_private_free(struct snd_kcontrol *kctl)
{
struct snd_jack_kctl *jack_kctl;
jack_kctl = kctl->private_data;
if (jack_kctl) {
snd_jack_debugfs_clear_inject_node(jack_kctl);
list_del(&jack_kctl->list);
kfree(jack_kctl);
}
@ -122,7 +409,9 @@ static void snd_jack_kctl_private_free(struct snd_kcontrol *kctl)
static void snd_jack_kctl_add(struct snd_jack *jack, struct snd_jack_kctl *jack_kctl)
{
jack_kctl->jack = jack;
list_add_tail(&jack_kctl->list, &jack->kctl_list);
snd_jack_debugfs_add_inject_node(jack, jack_kctl);
}
static struct snd_jack_kctl * snd_jack_kctl_new(struct snd_card *card, const char *name, unsigned int mask)
@ -340,6 +629,7 @@ EXPORT_SYMBOL(snd_jack_set_key);
void snd_jack_report(struct snd_jack *jack, int status)
{
struct snd_jack_kctl *jack_kctl;
unsigned int mask_bits = 0;
#ifdef CONFIG_SND_JACK_INPUT_DEV
int i;
#endif
@ -347,16 +637,21 @@ void snd_jack_report(struct snd_jack *jack, int status)
if (!jack)
return;
jack->hw_status_cache = status;
list_for_each_entry(jack_kctl, &jack->kctl_list, list)
snd_kctl_jack_report(jack->card, jack_kctl->kctl,
status & jack_kctl->mask_bits);
if (jack_kctl->sw_inject_enable)
mask_bits |= jack_kctl->mask_bits;
else
snd_kctl_jack_report(jack->card, jack_kctl->kctl,
status & jack_kctl->mask_bits);
#ifdef CONFIG_SND_JACK_INPUT_DEV
if (!jack->input_dev)
return;
for (i = 0; i < ARRAY_SIZE(jack->key); i++) {
int testbit = SND_JACK_BTN_0 >> i;
int testbit = ((SND_JACK_BTN_0 >> i) & ~mask_bits);
if (jack->type & testbit)
input_report_key(jack->input_dev, jack->key[i],
@ -364,7 +659,8 @@ void snd_jack_report(struct snd_jack *jack, int status)
}
for (i = 0; i < ARRAY_SIZE(jack_switch_types); i++) {
int testbit = 1 << i;
int testbit = ((1 << i) & ~mask_bits);
if (jack->type & testbit)
input_report_switch(jack->input_dev,
jack_switch_types[i],

View File

@ -9,6 +9,7 @@
#include <linux/time.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/debugfs.h>
#include <sound/core.h>
#include <sound/minors.h>
#include <sound/info.h>
@ -39,6 +40,11 @@ MODULE_ALIAS_CHARDEV_MAJOR(CONFIG_SND_MAJOR);
int snd_ecards_limit;
EXPORT_SYMBOL(snd_ecards_limit);
#ifdef CONFIG_SND_DEBUG
struct dentry *sound_debugfs_root;
EXPORT_SYMBOL_GPL(sound_debugfs_root);
#endif
static struct snd_minor *snd_minors[SNDRV_OS_MINORS];
static DEFINE_MUTEX(sound_mutex);
@ -395,6 +401,10 @@ static int __init alsa_sound_init(void)
unregister_chrdev(major, "alsa");
return -ENOMEM;
}
#ifdef CONFIG_SND_DEBUG
sound_debugfs_root = debugfs_create_dir("sound", NULL);
#endif
#ifndef MODULE
pr_info("Advanced Linux Sound Architecture Driver Initialized.\n");
#endif
@ -403,6 +413,9 @@ static int __init alsa_sound_init(void)
static void __exit alsa_sound_exit(void)
{
#ifdef CONFIG_SND_DEBUG
debugfs_remove(sound_debugfs_root);
#endif
snd_info_done();
unregister_chrdev(major, "alsa");
}