[alsa-devel] [PATCH] Softvol: Allow per process volume control.

Jimmy jimmy at spalge.com
Thu Jul 9 04:18:48 CEST 2015


This change uses ephemeral pcm controls to provide per process volume
control in conjunction with dmix. It basically generates a new control
every time a softvol PCM is opened and deletes it afterwards. It also
needs to generate a unique name for the control and I have included one
method of doing that. This patch is totally non-breaking and backwards
compatible, apart from one call to snd_ctl_elem_unlock (see below).

Changes to asoundrc:

* pcm.softvol.per_process BOOL has been added to enable these changes,
  defaults to false.
* when per_process is true then bits of text in pcm.softvol.control.name
  surrounded by curly braces ('{' and '}') are treated as substitution
  variables (eg "softvol.{pid}") so that uniquely named controls can be
  generated.

Changes to pcm_softvol.c

* new int (really a bool) in snd_pcm_softvol_t that is read in
  _...open() and passed through to the internal open method because that
  seemed to be the thing to do and then used in softvol_load_control().
* a big ugly per_proc_format_ctl_name() function has been added that
  generates the new hopefully unique names. It talks to /proc/ and does
  a bunch string manipulation. There probably are no off-by-one errors
  left in there.
* per process controls are closed along with the pcm.
* I added a call to snd_ctl_elem_unlock() when a new control is created
  and *didn't* put a guard on it because I don't understand why anyone
  would want the control locked anyway ... It seems that the first time
  that softvol is opened you can't use the control so you have to close
  and open it again. Presumably there is a reason for this because
  unlock isn't called anywhere else, but I don't know what it is. Nothing
  else calls unlock either, maybe nothing else makes new on-demand ctls.

I'm open to suggestions like "get rid of the templating" and "use better
config variable names" but as far as I can tell the basic idea is useful
and simple.
---
 src/pcm/pcm_softvol.c | 148 ++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 144 insertions(+), 4 deletions(-)

diff --git a/src/pcm/pcm_softvol.c b/src/pcm/pcm_softvol.c
index c6cfd8896b26..a03b672a6f42 100644
--- a/src/pcm/pcm_softvol.c
+++ b/src/pcm/pcm_softvol.c
@@ -31,6 +31,11 @@
 #include "pcm_local.h"
 #include "pcm_plugin.h"
 
+#include <sys/types.h>
+#include <unistd.h>
+#include <errno.h>
+#include <libgen.h> //basename
+
 #ifndef PIC
 /* entry for static linking */
 const char *_snd_module_pcm_softvol = "";
@@ -51,6 +56,7 @@ typedef struct {
 	double min_dB;
 	double max_dB;
 	unsigned int *dB_value;
+	int per_process;
 } snd_pcm_softvol_t;
 
 #define VOL_SCALE_SHIFT		16
@@ -408,6 +414,17 @@ static void softvol_free(snd_pcm_softvol_t *svol)
 static int snd_pcm_softvol_close(snd_pcm_t *pcm)
 {
 	snd_pcm_softvol_t *svol = pcm->private_data;
+	snd_ctl_elem_info_t *cinfo;
+
+	if (svol->per_process) {
+		snd_ctl_elem_info_alloca(&cinfo);
+		snd_ctl_elem_info_set_id(cinfo, &svol->elem.id);
+		// Ignore non-existence at this point.
+		if (!snd_ctl_elem_info(svol->ctl, cinfo)) {
+			snd_ctl_elem_remove(svol->ctl, &cinfo->id);
+		}
+	}
+
 	softvol_free(svol);
 	return 0;
 }
@@ -689,6 +706,92 @@ static int add_user_ctl(snd_pcm_softvol_t *svol, snd_ctl_elem_info_t *cinfo, int
 	return snd_ctl_elem_write(svol->ctl, &svol->elem);
 }
 
+/* Takes a template string of the form "softvol {cmd} ({pid})" and writes to
+ * outbuf a the same string with the text within curly braces replaced with
+ * relevant information. Returns the number of chars written to outbuf
+ * including a terminating '\0' or a negative number on error.
+ *
+ * Currently supported values for substition are:
+ * pid  The current processes pid
+ * cmd  The basename of the processes argv[0], from /proc/self/cmdline
+ * exe  The basename of the processes binary, from following /proc/self/exe
+ * {    The literal open curly brace '{'
+ * ob   The literal open curly brace '{'
+ * cb   The literal open curly brace '}'
+ */
+static int per_proc_format_ctl_name(char *outbuf, size_t outbuf_len, char *template) {
+	int template_len = strlen(template), n, proc_fd;
+	char *outbuf_p = outbuf, *open_b, *close_b, *template_p = template;
+	char proc_buf[50]; // max pid can be up to 22 bits ...
+	char resolved_path[PATH_MAX];
+
+	// walk through template_p pushing onto outbuf_p
+	while (outbuf_p < outbuf+outbuf_len) {
+		open_b = strchr(template_p, '{');
+		if (open_b == NULL) {
+			// Point the end of the current literal to the end
+			open_b = template + template_len;
+		}
+		memcpy(outbuf_p, template_p, open_b-template_p);
+		outbuf_p += open_b-template_p;
+		template_p = open_b+1;
+
+		if (template_p >= template + template_len - 1) {
+			*outbuf_p = '\0';
+			break;
+		}
+
+		if ((close_b = strchr(open_b, '}')) == NULL) {
+			SNDERR("Invalid control name template: Unmatched '{'");
+			return -EINVAL;
+		}
+		*close_b = '\0';
+		if (strcmp(template_p, "pid") == 0) {
+			n = snprintf(outbuf_p, outbuf+outbuf_len-outbuf_p, "%d", getpid());
+		} else if (strcmp(template_p, "cmd") == 0) {
+			n = snprintf(proc_buf, 49, "/proc/%d/cmdline", getpid());
+			proc_fd = open(proc_buf, O_RDONLY);
+			if (proc_fd < 0) {
+				// Maybe a short lived process?
+				n = errno;
+				SNDERR("Unable to complete control name template: open: %s: %s", strerror(errno), proc_buf);
+				return -n;
+			}
+			n = read(proc_fd, proc_buf, 49);
+			proc_buf[n] = '\0';
+			n = basename(proc_buf)-proc_buf;
+			open_b = strchr(proc_buf+n, ' ');
+			if (open_b != NULL) *open_b = '\0';
+			n = snprintf(outbuf_p, outbuf+outbuf_len-outbuf_p, "%s", proc_buf+n);
+		} else if (strcmp(template_p, "exe") == 0) {
+			n = snprintf(proc_buf, 49, "/proc/%d/exe", getpid());
+			if (realpath(proc_buf, resolved_path) == NULL) {
+				// Maybe a short lived process?
+				n = errno;
+				SNDERR("Unable to complete control name template: open: %s: %s", strerror(errno), proc_buf);
+				return -n;
+			}
+			n = basename(resolved_path)-resolved_path;
+			n = snprintf(outbuf_p, outbuf+outbuf_len-outbuf_p, "%s", basename(resolved_path));
+		} else if (strcmp(template_p, "{") == 0 || strcmp(template_p, "ob") == 0) {
+			n = 1;
+			*outbuf_p = '{';
+		} else if (strcmp(template_p, "cb") == 0) {
+			n = 1;
+			*outbuf_p = '}';
+		} else if (strlen(template_p) == 0) {
+			n = 0;
+		} else {
+			SNDERR("Invalid control name template: Unknown substitution: %s", template_p);
+			return -EINVAL;
+		}
+
+		outbuf_p = outbuf_p + n;
+		template_p = close_b + 1;
+	}
+	return strlen(outbuf)+1;
+}
+
 /*
  * load and set up user-control
  * returns 0 if the user-control is found or created,
@@ -700,7 +803,7 @@ static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 				int cchannels, double min_dB, double max_dB,
 				int resolution)
 {
-	char tmp_name[32];
+	char tmp_name[32], tmp_template[sizeof(ctl_id->name)];
 	snd_pcm_info_t *info;
 	snd_ctl_elem_info_t *cinfo;
 	int err;
@@ -724,6 +827,15 @@ static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 		return err;
 	}
 
+	if (svol->per_process) {
+		err = per_proc_format_ctl_name(tmp_template, sizeof(ctl_id->name), ctl_id->name);
+		if (err < 1) { // Shouldn't be zero length output...
+			SNDERR("Cannot open CTL %s: Control name template error %d", tmp_name, err);
+			return -EINVAL;
+		}
+		memcpy(&ctl_id->name, tmp_template, sizeof(ctl_id->name));
+	}
+
 	svol->elem.id = *ctl_id;
 	svol->max_val = resolution - 1;
 	svol->min_dB = min_dB;
@@ -747,6 +859,7 @@ static int softvol_load_control(snd_pcm_t *pcm, snd_pcm_softvol_t *svol,
 			SNDERR("Cannot add a control");
 			return err;
 		}
+		snd_ctl_elem_unlock(svol->ctl, &cinfo->id);
 	} else {
 		if (! (cinfo->access & SNDRV_CTL_ELEM_ACCESS_USER)) {
 			/* hardware control exists */
@@ -836,6 +949,10 @@ static const snd_pcm_ops_t snd_pcm_softvol_ops = {
  * \param resolution resolution of control
  * \param slave Slave PCM handle
  * \param close_slave When set, the slave PCM handle is closed with copy PCM
+ * \param per_process When set, a new ephemeral control is created for each
+ *        softvol PCM. This allows per process volume control when used with
+ *        dmix. Make sure the control name has eg {pid}, {cmd}, {exe} in it so
+ *        the generated names are unique.
  * \retval zero on success otherwise a negative error code
  * \warning Using of this function might be dangerous in the sense
  *          of compatibility reasons. The prototype might be freely
@@ -846,7 +963,7 @@ int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 			 int ctl_card, snd_ctl_elem_id_t *ctl_id,
 			 int cchannels,
 			 double min_dB, double max_dB, int resolution,
-			 snd_pcm_t *slave, int close_slave)
+			 snd_pcm_t *slave, int close_slave, int per_process)
 {
 	snd_pcm_t *pcm;
 	snd_pcm_softvol_t *svol;
@@ -862,6 +979,7 @@ int snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 	svol = calloc(1, sizeof(*svol));
 	if (! svol)
 		return -ENOMEM;
+	svol->per_process = per_process;
 	err = softvol_load_control(slave, svol, ctl_card, ctl_id, cchannels,
 				   min_dB, max_dB, resolution);
 	if (err < 0) {
@@ -929,6 +1047,17 @@ 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.
 
+If per_process is set you can (and should) user certain substition variables
+in the control name so that uniquely named controls can be generated per-process.
+Substition variables are within curly braces, everything else is used literaly, eg
+{cmd} ({pid}). Currently supported variables are:
+- pid  The current processes pid
+- cmd  The basename of the processes argv[0], from /proc/self/cmdline
+- exe  The basename of the processes binary, from following /proc/self/exe
+- {    The literal open curly brace '{'
+- ob   The literal open curly brace '{'
+- cb   The literal open curly brace '}'
+
 \code
 pcm.name {
         type softvol            # Soft Volume conversion PCM
@@ -953,6 +1082,8 @@ pcm.name {
 	[max_dB REAL]           # maximal dB value (default:   0.0)
 	[resolution INT]        # resolution (default: 256)
 				# resolution = 2 means a mute switch
+	[per_process BOOL]      # generate one control per process (default: false)
+				# useful with dmix.
 }
 \endcode
 
@@ -992,7 +1123,7 @@ int _snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 	int resolution = PRESET_RESOLUTION;
 	double min_dB = PRESET_MIN_DB;
 	double max_dB = ZERO_DB;
-	int card = -1, cchannels = 2;
+	int card = -1, cchannels = 2, per_process = 0;
 
 	snd_config_for_each(i, next, conf) {
 		snd_config_t *n = snd_config_iterator_entry(i);
@@ -1035,6 +1166,15 @@ int _snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 			}
 			continue;
 		}
+		if (strcmp(id, "per_process") == 0) {
+			err = snd_config_get_bool(n);
+			if (err < 0) {
+				SNDERR("Invalid boolean value in per_process");
+				return err;
+			}
+			per_process = err;
+			continue;
+		}
 		SNDERR("Unknown field %s", id);
 		return -EINVAL;
 	}
@@ -1092,7 +1232,7 @@ int _snd_pcm_softvol_open(snd_pcm_t **pcmp, const char *name,
 			return err;
 		}
 		err = snd_pcm_softvol_open(pcmp, name, sformat, card, ctl_id, cchannels,
-					   min_dB, max_dB, resolution, spcm, 1);
+					   min_dB, max_dB, resolution, spcm, 1, per_process);
 		if (err < 0)
 			snd_pcm_close(spcm);
 	}
-- 
2.1.4



More information about the Alsa-devel mailing list