diff mbox series

Add support to expose controls of ladspa plugin

Message ID 20210201150352.6342-1-camel.guo@axis.com
State New
Headers show
Series Add support to expose controls of ladspa plugin | expand

Commit Message

Camel Guo Feb. 1, 2021, 3:03 p.m. UTC
From: Camel Guo <camelg@axis.com>

In order for external software components to adjust ladspa plugin
dynamically, this commit adds an option to exposes the control array of
input control ports of a ladspa plugin to a file, through which any
applications with proper permission can control this plugin.

Signed-off-by: Camel Guo <camelg@axis.com>
---
 src/pcm/pcm_ladspa.c | 157 ++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 139 insertions(+), 18 deletions(-)

Comments

Camel Guo Feb. 2, 2021, 8:12 a.m. UTC | #1
On 2/1/21 4:32 PM, Jaroslav Kysela wrote:
> Dne 01. 02. 21 v 16:03 Camel Guo napsal(a):
>> From: Camel Guo <camelg@axis.com>
>> 
>> In order for external software components to adjust ladspa plugin
>> dynamically, this commit adds an option to exposes the control array of
>> input control ports of a ladspa plugin to a file, through which any
>> applications with proper permission can control this plugin.
> 
> It looks like a pure hack (although the implementation is interesting). The
> controls may be exposed via the ctl (control) API like we do in
> src/pcm/pcm_softvol.c for example. The floats can be mapped to integer64 
> or we
> may discuss to add the float type to the control API elements.

If there are not so many input controls of a ladspa plugin, I think it 
is okay to implement it like pcm_softvol.c. But the problem is that some 
plugins might have more than 100 input controls. For every input 
control, there will be a system call ioctl in order to get its value. 
This will make performance really bad. If a ladspa plugin like this 
needs to support per-channel control, that will make it even worst.

But with shared memory like this, it will become a pure memory read, 
this will make performance acceptable.

Another benefit of exposing ladspa control array to shared memory  is 
that it makes it possible for any algorithms to connect with these 
ladspa plugins. The ladspa control array is basically a float-array, 
which is exactly the input, output of lots of machine learning, deep 
learning algorithm. Imagine an algorithm listening to the audio stream 
automatically applies privacy masks on audio stream to mask human voice 
via ladspa plugins.

> 
>                                                  Jaroslav
> 
> -- 
> Jaroslav Kysela <perex@perex.cz>
> Linux Sound Maintainer; ALSA Project; Red Hat, Inc.
diff mbox series

Patch

diff --git a/src/pcm/pcm_ladspa.c b/src/pcm/pcm_ladspa.c
index ad73347d..40b5d38f 100644
--- a/src/pcm/pcm_ladspa.c
+++ b/src/pcm/pcm_ladspa.c
@@ -32,6 +32,9 @@ 
  *   http://www.medianet.ag
  */
   
+#include <stdbool.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
 #include <dirent.h>
 #include <locale.h>
 #include <math.h>
@@ -93,7 +96,8 @@  typedef struct {
 	unsigned int port_bindings_size;	/* size of array */
 	unsigned int *port_bindings;		/* index = channel number, value = LADSPA port */
 	unsigned int controls_size;		/* size of array */
-	unsigned char *controls_initialized;	/* initialized by ALSA user */
+	bool controls_new;			/* if controls is new, it can be overrided by alsa config */
+	bool controls_shared;			/* if controls is shared memory map */
 	LADSPA_Data *controls;			/* index = LADSPA control port index */
 } snd_pcm_ladspa_plugin_io_t;
 
@@ -101,6 +105,7 @@  typedef struct {
 	struct list_head list;
 	snd_pcm_ladspa_policy_t policy;
 	char *filename;
+	char *controls_path;
 	void *dl_handle;
 	const LADSPA_Descriptor *desc;
 	snd_pcm_ladspa_plugin_io_t input;
@@ -110,6 +115,73 @@  typedef struct {
 
 #endif /* DOC_HIDDEN */
 
+static LADSPA_Data* mmap_ladspa_controls(const char* filename, unsigned long length, bool *is_new)
+{
+	LADSPA_Data *ptr = NULL;
+	int fd = -1;
+	int ret = 0;
+	struct stat statbuf = { 0 };
+	int prot = PROT_READ;
+
+	if (filename == NULL || is_new == NULL) return NULL;
+
+	*is_new = false;
+
+	fd = open(filename, O_RDONLY);
+	if(fd < 0) {
+		if (errno == ENOENT) {
+			fd = open(filename, O_RDWR | O_CREAT, 0666);
+			if(fd < 0) {
+				SNDERR("Failed to open controls file: %s", filename);
+				return NULL;
+			}
+
+			prot |= PROT_WRITE;
+			*is_new = true;
+		} else {
+			SNDERR("Failed to open %s due to '%s'", filename, strerror(errno));
+			return NULL;
+		}
+	}
+
+	ret = fstat(fd, &statbuf);
+	if (ret == -1) {
+		SNDERR("Failed to get status of '%s' due to '%s'", filename, strerror(errno));
+		goto out;
+	}
+
+	if (statbuf.st_size < (off_t) length) {
+		/* this file is invalid, must be truncated and re-initialized later on */
+		close(fd);
+
+		fd = open(filename, O_RDWR | O_CREAT, 0666);
+		if(fd < 0) {
+			SNDERR("Failed to open controls file: %s", filename);
+			/* no need to close(fd), so just return */
+			return NULL;
+		}
+
+		ret = ftruncate(fd, length);
+		if (ret == -1) {
+			SNDERR("Failed to increase the size of '%s' due to '%s'", filename, strerror(errno));
+			goto out;
+		}
+
+		prot |= PROT_WRITE;
+		*is_new = true;
+	}
+
+	ptr = (LADSPA_Data*) mmap(NULL, length, prot, MAP_SHARED, fd, 0);
+	if(ptr == MAP_FAILED) {
+		SNDERR("Failed to mmap %s due to %s", filename, strerror(errno));
+		ptr = NULL;
+	}
+
+out:
+	close (fd);
+	return ptr;
+}
+
 static unsigned int snd_pcm_ladspa_count_ports(snd_pcm_ladspa_plugin_t *lplug,
                                                LADSPA_PortDescriptor pdesc)
 {
@@ -174,8 +246,11 @@  static int snd_pcm_ladspa_find_port_idx(unsigned int *res,
 
 static void snd_pcm_ladspa_free_io(snd_pcm_ladspa_plugin_io_t *io)
 {
-	free(io->controls);
-	free(io->controls_initialized);
+	if (io->controls_shared) {
+		munmap(io->controls, io->controls_size * sizeof(LADSPA_Data));
+	} else {
+		free(io->controls);
+	}
 }
 
 static void snd_pcm_ladspa_free_plugins(struct list_head *plugins)
@@ -186,6 +261,8 @@  static void snd_pcm_ladspa_free_plugins(struct list_head *plugins)
                 snd_pcm_ladspa_free_io(&plugin->output);
 		if (plugin->dl_handle)
 			dlclose(plugin->dl_handle);
+		if (plugin->controls_path)
+			free(plugin->controls_path);
 		free(plugin->filename);
 		list_del(&plugin->list);
 		free(plugin);
@@ -574,8 +651,6 @@  static int snd_pcm_ladspa_connect_controls(snd_pcm_ladspa_plugin_t *plugin,
 	for (idx = midx = 0; idx < plugin->desc->PortCount; idx++)
 		if ((plugin->desc->PortDescriptors[idx] & (io->pdesc | LADSPA_PORT_CONTROL)) == (io->pdesc | LADSPA_PORT_CONTROL)) {
 			if (io->controls_size > midx) {
-			        if (!io->controls_initialized[midx])
-			                snd_pcm_ladspa_get_default_cvalue(plugin->desc, idx, &io->controls[midx]);
 				plugin->desc->connect_port(instance->handle, idx, &io->controls[midx]);
 			} else {
 				return -EINVAL;
@@ -878,6 +953,10 @@  snd_pcm_ladspa_write_areas(snd_pcm_t *pcm,
         		snd_pcm_ladspa_plugin_t *plugin = list_entry(pos, snd_pcm_ladspa_plugin_t, list);
         		list_for_each(pos1, &plugin->instances) {
         			instance = list_entry(pos1, snd_pcm_ladspa_instance_t, list);
+				if (plugin->input.controls_shared) {
+					(void) snd_pcm_ladspa_connect_controls(plugin, &plugin->input, instance);
+				}
+				/* Skip output controls since they can not be changed dynamically */
         			for (idx = 0; idx < instance->input.channels.size; idx++) {
                                         chn = instance->input.channels.array[idx];
                                         data = instance->input.data[idx];
@@ -939,6 +1018,10 @@  snd_pcm_ladspa_read_areas(snd_pcm_t *pcm,
         		snd_pcm_ladspa_plugin_t *plugin = list_entry(pos, snd_pcm_ladspa_plugin_t, list);
         		list_for_each(pos1, &plugin->instances) {
         			instance = list_entry(pos1, snd_pcm_ladspa_instance_t, list);
+				if (plugin->input.controls_shared) {
+					(void) snd_pcm_ladspa_connect_controls(plugin, &plugin->input, instance);
+				}
+				/* Skip output controls since they can not be changed dynamically */
         			for (idx = 0; idx < instance->input.channels.size; idx++) {
                                         chn = instance->input.channels.array[idx];
                                         data = instance->input.data[idx];
@@ -1227,26 +1310,50 @@  static int snd_pcm_ladspa_add_default_controls(snd_pcm_ladspa_plugin_t *lplug,
 {
 	unsigned int count = 0;
 	LADSPA_Data *array;
-	unsigned char *initialized;
 	unsigned long idx;
+	unsigned long midx;
+	bool controls_shared = false;
+	bool is_new = true;
 
 	for (idx = 0; idx < lplug->desc->PortCount; idx++)
 		if ((lplug->desc->PortDescriptors[idx] & (io->pdesc | LADSPA_PORT_CONTROL)) == (io->pdesc | LADSPA_PORT_CONTROL))
 			count++;
-	array = (LADSPA_Data *)calloc(count, sizeof(LADSPA_Data));
-	if (!array)
-		return -ENOMEM;
-	initialized = (unsigned char *)calloc(count, sizeof(unsigned char));
-	if (!initialized) {
-		free(array);
-		return -ENOMEM;
+
+	/*
+	 * Only support to expose ladspa control array for input control ports
+	 */
+	if ((io->pdesc == LADSPA_PORT_INPUT) && lplug->controls_path && count != 0) {
+		array = mmap_ladspa_controls(lplug->controls_path, count * sizeof(LADSPA_Data), &is_new);
+		if (!array)
+			return -ENOMEM;
+		controls_shared = true;
+	} else {
+		array = (LADSPA_Data *)calloc(count, sizeof(LADSPA_Data));
+		if (!array)
+			return -ENOMEM;
+	}
+
+	if (is_new) {
+		/*
+		 * Initialize array of controls of this ladspa plugin with its
+		 * default values defined in this plugin implementation. This
+		 * array can be overrided by its alsa configuration files.
+		 */
+		for (idx = midx = 0; idx < lplug->desc->PortCount; idx++) {
+			if ((lplug->desc->PortDescriptors[idx] & (io->pdesc | LADSPA_PORT_CONTROL)) == (io->pdesc | LADSPA_PORT_CONTROL)) {
+				snd_pcm_ladspa_get_default_cvalue(lplug->desc, idx, &array[midx]);
+				midx++;
+			}
+		}
 	}
+
 	io->controls_size = count;
-	io->controls_initialized = initialized;
+	io->controls_new = is_new;
+	io->controls_shared = controls_shared;
 	io->controls = array;
 
 	return 0;
-}	
+}
 
 static int snd_pcm_ladspa_parse_controls(snd_pcm_ladspa_plugin_t *lplug,
 					 snd_pcm_ladspa_plugin_io_t *io,
@@ -1287,8 +1394,10 @@  static int snd_pcm_ladspa_parse_controls(snd_pcm_ladspa_plugin_t *lplug,
 			SNDERR("internal error");
 			return err;
 		}
-		io->controls_initialized[uval] = 1;
-		io->controls[uval] = (LADSPA_Data)dval;
+
+		if (io->controls_new) {
+			io->controls[uval] = (LADSPA_Data)dval;
+		}
 	}
 
 	return 0;
@@ -1429,7 +1538,7 @@  static int snd_pcm_ladspa_add_plugin(struct list_head *list,
 				     int reverse)
 {
 	snd_config_iterator_t i, next;
-	const char *label = NULL, *filename = NULL;
+	const char *label = NULL, *filename = NULL, *controls_path = NULL;
 	long ladspa_id = 0;
 	int err;
 	snd_pcm_ladspa_plugin_t *lplug;
@@ -1467,6 +1576,12 @@  static int snd_pcm_ladspa_add_plugin(struct list_head *list,
 			output = n;
 			continue;
 		}
+		if (strcmp(id, "controls") == 0) {
+			err = snd_config_get_string(n, &controls_path);
+			if (err < 0)
+				return err;
+			continue;
+		}
 		if (strcmp(id, "policy") == 0) {
 			const char *str;
 			err = snd_config_get_string(n, &str);
@@ -1496,6 +1611,11 @@  static int snd_pcm_ladspa_add_plugin(struct list_head *list,
 	lplug->input.pdesc = LADSPA_PORT_INPUT;
 	lplug->output.pdesc = LADSPA_PORT_OUTPUT;
 	INIT_LIST_HEAD(&lplug->instances);
+	if (controls_path) {
+		lplug->controls_path = strdup(controls_path);
+	} else {
+		lplug->controls_path = NULL;
+	}
 	if (filename) {
 		err = snd_pcm_ladspa_check_file(lplug, filename, label, ladspa_id);
 		if (err < 0) {
@@ -1692,6 +1812,7 @@  pcm.name {
 			[label STR]	# LADSPA plugin label (for example 'delay_5s')
 			[filename STR]	# Full filename of .so library with LADSPA plugin code
 			[policy STR]	# Policy can be 'none' or 'duplicate'
+			[controls STR]	# Path (directory) with controls for this plugin
 			input | output {
 				bindings {
 					C INT or STR	# C - channel, INT - audio port index, STR - audio port name