mirror of
				https://github.com/alsa-project/alsa-lib.git
				synced 2025-11-03 09:01:52 -05:00 
			
		
		
		
	Change the behavior with hardware volume controls
When a hardware volume control is given, softvol plugin simply passes the slave PCM without any additional changes.
This commit is contained in:
		
							parent
							
								
									0732cce6f0
								
							
						
					
					
						commit
						199d207423
					
				
					 1 changed files with 70 additions and 42 deletions
				
			
		| 
						 | 
					@ -91,6 +91,11 @@ static unsigned short preset_dB_value[PRESET_RESOLUTION] = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#endif /* DOC_HIDDEN */
 | 
					#endif /* DOC_HIDDEN */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * apply volumue attenuation
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * TODO: use SIMD operations
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
static void snd_pcm_softvol_convert(snd_pcm_softvol_t *svol,
 | 
					static void snd_pcm_softvol_convert(snd_pcm_softvol_t *svol,
 | 
				
			||||||
				    const snd_pcm_channel_area_t *dst_areas,
 | 
									    const snd_pcm_channel_area_t *dst_areas,
 | 
				
			||||||
				    snd_pcm_uframes_t dst_offset,
 | 
									    snd_pcm_uframes_t dst_offset,
 | 
				
			||||||
| 
						 | 
					@ -134,6 +139,11 @@ static void snd_pcm_softvol_convert(snd_pcm_softvol_t *svol,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * get the current volume value from driver
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * TODO: mmap support?
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
static unsigned int get_current_volume(snd_pcm_softvol_t *svol)
 | 
					static unsigned int get_current_volume(snd_pcm_softvol_t *svol)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	unsigned int val;
 | 
						unsigned int val;
 | 
				
			||||||
| 
						 | 
					@ -145,17 +155,21 @@ static unsigned int get_current_volume(snd_pcm_softvol_t *svol)
 | 
				
			||||||
	return val;
 | 
						return val;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static int snd_pcm_softvol_close(snd_pcm_t *pcm)
 | 
					static void softvol_free(snd_pcm_softvol_t *svol)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	snd_pcm_softvol_t *svol = pcm->private_data;
 | 
					 | 
				
			||||||
	int err = 0;
 | 
					 | 
				
			||||||
	if (svol->plug.close_slave)
 | 
						if (svol->plug.close_slave)
 | 
				
			||||||
		err = snd_pcm_close(svol->plug.slave);
 | 
							snd_pcm_close(svol->plug.slave);
 | 
				
			||||||
	if (svol->ctl)
 | 
						if (svol->ctl)
 | 
				
			||||||
		snd_ctl_close(svol->ctl);
 | 
							snd_ctl_close(svol->ctl);
 | 
				
			||||||
	if (svol->dB_value && svol->dB_value != preset_dB_value)
 | 
						if (svol->dB_value && svol->dB_value != preset_dB_value)
 | 
				
			||||||
		free(svol->dB_value);
 | 
							free(svol->dB_value);
 | 
				
			||||||
	free(svol);
 | 
						free(svol);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int snd_pcm_softvol_close(snd_pcm_t *pcm)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						snd_pcm_softvol_t *svol = pcm->private_data;
 | 
				
			||||||
 | 
						softvol_free(svol);
 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -330,40 +344,42 @@ static void snd_pcm_softvol_dump(snd_pcm_t *pcm, snd_output_t *out)
 | 
				
			||||||
	snd_pcm_dump(svol->plug.slave, out);
 | 
						snd_pcm_dump(svol->plug.slave, out);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
int snd_ctl_elem_add(snd_ctl_t *ctl, snd_ctl_elem_info_t *info);
 | 
					 | 
				
			||||||
int snd_ctl_elem_replace(snd_ctl_t *ctl, snd_ctl_elem_info_t *info);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static int add_user_ctl(snd_pcm_softvol_t *svol, snd_ctl_elem_info_t *cinfo)
 | 
					static int add_user_ctl(snd_pcm_softvol_t *svol, snd_ctl_elem_info_t *cinfo)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	return snd_ctl_elem_add_integer(svol->ctl, &cinfo->id, 1, 0, svol->max_val, 0);
 | 
						return snd_ctl_elem_add_integer(svol->ctl, &cinfo->id, 1, 0, svol->max_val, 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * load and set up user-control
 | 
				
			||||||
 | 
					 * returns 0 if the user-control is found or created,
 | 
				
			||||||
 | 
					 * returns 1 if the control is a hw control,
 | 
				
			||||||
 | 
					 * or a negative error code
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 | 
					static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 | 
				
			||||||
				char *ctl_name, snd_ctl_elem_id_t *ctl_id,
 | 
									int ctl_card, snd_ctl_elem_id_t *ctl_id,
 | 
				
			||||||
				double min_dB, int resolution)
 | 
									double min_dB, int resolution)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	char tmp_name[32];
 | 
						char tmp_name[32];
 | 
				
			||||||
	snd_pcm_info_t *info;
 | 
						snd_pcm_info_t *info;
 | 
				
			||||||
	snd_ctl_elem_info_t *cinfo;
 | 
						snd_ctl_elem_info_t *cinfo;
 | 
				
			||||||
	int err, card;
 | 
						int err;
 | 
				
			||||||
	unsigned int i;
 | 
						unsigned int i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (! ctl_name) {
 | 
						if (ctl_card < 0) {
 | 
				
			||||||
		snd_pcm_info_alloca(&info);
 | 
							snd_pcm_info_alloca(&info);
 | 
				
			||||||
		err = snd_pcm_info(pcm, info);
 | 
							err = snd_pcm_info(pcm, info);
 | 
				
			||||||
		if (err < 0)
 | 
							if (err < 0)
 | 
				
			||||||
			return err;
 | 
								return err;
 | 
				
			||||||
		card = snd_pcm_info_get_card(info);
 | 
							ctl_card = snd_pcm_info_get_card(info);
 | 
				
			||||||
		if (card < 0) {
 | 
							if (ctl_card < 0) {
 | 
				
			||||||
			SNDERR("No card for this PCM");
 | 
								SNDERR("No card defined for softvol control");
 | 
				
			||||||
			return -EINVAL;
 | 
								return -EINVAL;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		sprintf(tmp_name, "hw:%d", card);
 | 
					 | 
				
			||||||
		ctl_name = tmp_name;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = snd_ctl_open(&svol->ctl, ctl_name, 0);
 | 
						sprintf(tmp_name, "hw:%d", ctl_card);
 | 
				
			||||||
 | 
						err = snd_ctl_open(&svol->ctl, tmp_name, 0);
 | 
				
			||||||
	if (err < 0) {
 | 
						if (err < 0) {
 | 
				
			||||||
		SNDERR("Cannot open CTL %s", ctl_name);
 | 
							SNDERR("Cannot open CTL %s", tmp_name);
 | 
				
			||||||
		return err;
 | 
							return err;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -375,7 +391,7 @@ static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 | 
				
			||||||
	snd_ctl_elem_info_set_id(cinfo, ctl_id);
 | 
						snd_ctl_elem_info_set_id(cinfo, ctl_id);
 | 
				
			||||||
	if ((err = snd_ctl_elem_info(svol->ctl, cinfo)) < 0) {
 | 
						if ((err = snd_ctl_elem_info(svol->ctl, cinfo)) < 0) {
 | 
				
			||||||
		if (err != -ENOENT) {
 | 
							if (err != -ENOENT) {
 | 
				
			||||||
			SNDERR("Cannot get info for CTL %s", ctl_name);
 | 
								SNDERR("Cannot get info for CTL %s", tmp_name);
 | 
				
			||||||
			return err;
 | 
								return err;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = add_user_ctl(svol, cinfo);
 | 
							err = add_user_ctl(svol, cinfo);
 | 
				
			||||||
| 
						 | 
					@ -384,14 +400,14 @@ static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 | 
				
			||||||
			return err;
 | 
								return err;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if (cinfo->type != SND_CTL_ELEM_TYPE_INTEGER ||
 | 
							if (! (cinfo->access & SNDRV_CTL_ELEM_ACCESS_USER)) {
 | 
				
			||||||
 | 
								/* hardware control exists */
 | 
				
			||||||
 | 
								return 1; /* notify */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							} else if (cinfo->type != SND_CTL_ELEM_TYPE_INTEGER ||
 | 
				
			||||||
		    cinfo->count != 1 ||
 | 
							    cinfo->count != 1 ||
 | 
				
			||||||
		    cinfo->value.integer.min != 0 ||
 | 
							    cinfo->value.integer.min != 0 ||
 | 
				
			||||||
		    cinfo->value.integer.max != resolution - 1) {
 | 
							    cinfo->value.integer.max != resolution - 1) {
 | 
				
			||||||
			if (! (cinfo->access & SNDRV_CTL_ELEM_ACCESS_USER)) {
 | 
					 | 
				
			||||||
				SNDERR("Invalid control");
 | 
					 | 
				
			||||||
				return -EINVAL;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			snd_ctl_elem_remove(svol->ctl, &cinfo->id);
 | 
								snd_ctl_elem_remove(svol->ctl, &cinfo->id);
 | 
				
			||||||
			err = add_user_ctl(svol, cinfo);
 | 
								err = add_user_ctl(svol, cinfo);
 | 
				
			||||||
			if (err < 0) {
 | 
								if (err < 0) {
 | 
				
			||||||
| 
						 | 
					@ -446,7 +462,7 @@ static snd_pcm_ops_t snd_pcm_softvol_ops = {
 | 
				
			||||||
 * \param pcmp Returns created PCM handle
 | 
					 * \param pcmp Returns created PCM handle
 | 
				
			||||||
 * \param name Name of PCM
 | 
					 * \param name Name of PCM
 | 
				
			||||||
 * \param sformat Slave format
 | 
					 * \param sformat Slave format
 | 
				
			||||||
 * \param ctl_id Control ID
 | 
					 * \param card card index of the control
 | 
				
			||||||
 * \param min_dB minimal dB value
 | 
					 * \param min_dB minimal dB value
 | 
				
			||||||
 * \param resolution resolution of control
 | 
					 * \param resolution resolution of control
 | 
				
			||||||
 * \param slave Slave PCM handle
 | 
					 * \param slave Slave PCM handle
 | 
				
			||||||
| 
						 | 
					@ -458,7 +474,7 @@ static snd_pcm_ops_t snd_pcm_softvol_ops = {
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
					int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
				
			||||||
			 snd_pcm_format_t sformat,
 | 
								 snd_pcm_format_t sformat,
 | 
				
			||||||
			 char *ctl_name, snd_ctl_elem_id_t *ctl_id,
 | 
								 int ctl_card, snd_ctl_elem_id_t *ctl_id,
 | 
				
			||||||
			 double min_dB, int resolution,
 | 
								 double min_dB, int resolution,
 | 
				
			||||||
			 snd_pcm_t *slave, int close_slave)
 | 
								 snd_pcm_t *slave, int close_slave)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -471,6 +487,18 @@ int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
				
			||||||
	svol = calloc(1, sizeof(*svol));
 | 
						svol = calloc(1, sizeof(*svol));
 | 
				
			||||||
	if (! svol)
 | 
						if (! svol)
 | 
				
			||||||
		return -ENOMEM;
 | 
							return -ENOMEM;
 | 
				
			||||||
 | 
						err = softvol_load_control(slave, svol, ctl_card, ctl_id, min_dB, resolution);
 | 
				
			||||||
 | 
						if (err < 0) {
 | 
				
			||||||
 | 
							softvol_free(svol);
 | 
				
			||||||
 | 
							return err;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (err > 0) { /* hardware control - no need for softvol! */
 | 
				
			||||||
 | 
							softvol_free(svol);
 | 
				
			||||||
 | 
							*pcmp = slave; /* just pass the slave */
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* do softvol */
 | 
				
			||||||
	snd_pcm_plugin_init(&svol->plug);
 | 
						snd_pcm_plugin_init(&svol->plug);
 | 
				
			||||||
	svol->sformat = sformat;
 | 
						svol->sformat = sformat;
 | 
				
			||||||
	svol->plug.read = snd_pcm_softvol_read_areas;
 | 
						svol->plug.read = snd_pcm_softvol_read_areas;
 | 
				
			||||||
| 
						 | 
					@ -482,7 +510,7 @@ int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = snd_pcm_new(&pcm, SND_PCM_TYPE_SOFTVOL, name, slave->stream, slave->mode);
 | 
						err = snd_pcm_new(&pcm, SND_PCM_TYPE_SOFTVOL, name, slave->stream, slave->mode);
 | 
				
			||||||
	if (err < 0) {
 | 
						if (err < 0) {
 | 
				
			||||||
		free(svol);
 | 
							softvol_free(svol);
 | 
				
			||||||
		return err;
 | 
							return err;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	pcm->ops = &snd_pcm_softvol_ops;
 | 
						pcm->ops = &snd_pcm_softvol_ops;
 | 
				
			||||||
| 
						 | 
					@ -492,17 +520,15 @@ int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
				
			||||||
	pcm->poll_events = slave->poll_events;
 | 
						pcm->poll_events = slave->poll_events;
 | 
				
			||||||
	snd_pcm_set_hw_ptr(pcm, &svol->plug.hw_ptr, -1, 0);
 | 
						snd_pcm_set_hw_ptr(pcm, &svol->plug.hw_ptr, -1, 0);
 | 
				
			||||||
	snd_pcm_set_appl_ptr(pcm, &svol->plug.appl_ptr, -1, 0);
 | 
						snd_pcm_set_appl_ptr(pcm, &svol->plug.appl_ptr, -1, 0);
 | 
				
			||||||
	err = softvol_load_control(pcm, svol, ctl_name, ctl_id, min_dB, resolution);
 | 
					 | 
				
			||||||
	if (err < 0) {
 | 
					 | 
				
			||||||
		snd_pcm_close(pcm);
 | 
					 | 
				
			||||||
		return err;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	*pcmp = pcm;
 | 
						*pcmp = pcm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static int parse_control_id(snd_config_t *conf, snd_ctl_elem_id_t *ctl_id, char **ctl_name)
 | 
					/*
 | 
				
			||||||
 | 
					 * parse card index and id for the softvol control
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					static int parse_control_id(snd_config_t *conf, snd_ctl_elem_id_t *ctl_id, int *cardp)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	snd_config_iterator_t i, next;
 | 
						snd_config_iterator_t i, next;
 | 
				
			||||||
	int iface = SND_CTL_ELEM_IFACE_MIXER;
 | 
						int iface = SND_CTL_ELEM_IFACE_MIXER;
 | 
				
			||||||
| 
						 | 
					@ -512,7 +538,7 @@ static int parse_control_id(snd_config_t *conf, snd_ctl_elem_id_t *ctl_id, char
 | 
				
			||||||
	long subdevice = -1;
 | 
						long subdevice = -1;
 | 
				
			||||||
	int err;
 | 
						int err;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	*ctl_name = NULL;
 | 
						*cardp = -1;
 | 
				
			||||||
	snd_config_for_each(i, next, conf) {
 | 
						snd_config_for_each(i, next, conf) {
 | 
				
			||||||
		snd_config_t *n = snd_config_iterator_entry(i);
 | 
							snd_config_t *n = snd_config_iterator_entry(i);
 | 
				
			||||||
		const char *id;
 | 
							const char *id;
 | 
				
			||||||
| 
						 | 
					@ -521,14 +547,12 @@ static int parse_control_id(snd_config_t *conf, snd_ctl_elem_id_t *ctl_id, char
 | 
				
			||||||
		if (strcmp(id, "comment") == 0)
 | 
							if (strcmp(id, "comment") == 0)
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
		if (strcmp(id, "card") == 0) {
 | 
							if (strcmp(id, "card") == 0) {
 | 
				
			||||||
			const char *ptr;
 | 
								long v;
 | 
				
			||||||
			if ((err = snd_config_get_string(n, &ptr)) < 0) {
 | 
								if ((err = snd_config_get_integer(n, &v)) < 0) {
 | 
				
			||||||
				SNDERR("field %s is not a string", id);
 | 
									SNDERR("field %s is not an integer", id);
 | 
				
			||||||
				goto _err;
 | 
									goto _err;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (*ctl_name)
 | 
								*cardp = v;
 | 
				
			||||||
				free(*ctl_name);
 | 
					 | 
				
			||||||
			*ctl_name = strdup(ptr);
 | 
					 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (strcmp(id, "iface") == 0 || strcmp(id, "interface") == 0) {
 | 
							if (strcmp(id, "iface") == 0 || strcmp(id, "interface") == 0) {
 | 
				
			||||||
| 
						 | 
					@ -604,6 +628,10 @@ static int parse_control_id(snd_config_t *conf, snd_ctl_elem_id_t *ctl_id, char
 | 
				
			||||||
This plugin applies the software volume attenuation.
 | 
					This plugin applies the software volume attenuation.
 | 
				
			||||||
The format, rate and channels must match for both of source and destination.
 | 
					The format, rate and channels must match for both of source and destination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If the control already exists and it's a system control (i.e. no
 | 
				
			||||||
 | 
					user-defined control), the plugin simply passes its slave without
 | 
				
			||||||
 | 
					any changes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\code
 | 
					\code
 | 
				
			||||||
pcm.name {
 | 
					pcm.name {
 | 
				
			||||||
        type softvol            # Soft Volume conversion PCM
 | 
					        type softvol            # Soft Volume conversion PCM
 | 
				
			||||||
| 
						 | 
					@ -663,7 +691,7 @@ int _snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
				
			||||||
	snd_ctl_elem_id_t *ctl_id;
 | 
						snd_ctl_elem_id_t *ctl_id;
 | 
				
			||||||
	int resolution = PRESET_RESOLUTION;
 | 
						int resolution = PRESET_RESOLUTION;
 | 
				
			||||||
	double min_dB = PRESET_MIN_DB;
 | 
						double min_dB = PRESET_MIN_DB;
 | 
				
			||||||
	char *ctl_name;
 | 
						int card = -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	snd_config_for_each(i, next, conf) {
 | 
						snd_config_for_each(i, next, conf) {
 | 
				
			||||||
		snd_config_t *n = snd_config_iterator_entry(i);
 | 
							snd_config_t *n = snd_config_iterator_entry(i);
 | 
				
			||||||
| 
						 | 
					@ -731,11 +759,11 @@ int _snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 | 
				
			||||||
	if (err < 0)
 | 
						if (err < 0)
 | 
				
			||||||
		return err;
 | 
							return err;
 | 
				
			||||||
	snd_ctl_elem_id_alloca(&ctl_id);
 | 
						snd_ctl_elem_id_alloca(&ctl_id);
 | 
				
			||||||
	if ((err = parse_control_id(control, ctl_id, &ctl_name)) < 0) {
 | 
						if ((err = parse_control_id(control, ctl_id, &card)) < 0) {
 | 
				
			||||||
		snd_pcm_close(spcm);
 | 
							snd_pcm_close(spcm);
 | 
				
			||||||
		return err;
 | 
							return err;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = snd_pcm_softvol_open(pcmp, name, sformat, ctl_name, ctl_id, min_dB, resolution, spcm, 1);
 | 
						err = snd_pcm_softvol_open(pcmp, name, sformat, card, ctl_id, min_dB, resolution, spcm, 1);
 | 
				
			||||||
	if (err < 0)
 | 
						if (err < 0)
 | 
				
			||||||
		snd_pcm_close(spcm);
 | 
							snd_pcm_close(spcm);
 | 
				
			||||||
	return err;
 | 
						return err;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue