aboutsummaryrefslogtreecommitdiff
path: root/mod/ladspa.c
diff options
context:
space:
mode:
authorLudvig Gunne Lindström <ludviggunnelindstrom@gmail.com>2026-07-17 11:43:24 +0200
committerLudvig Gunne Lindström <ludviggunnelindstrom@gmail.com>2026-11-08 14:15:04 +0100
commit039847a29868d335c08fe0a587260dffc71ca5c4 (patch)
treee9d775090fef45ca51db576779fb373040d50e6a /mod/ladspa.c
downloadmelisma-modules-039847a29868d335c08fe0a587260dffc71ca5c4.tar.gz
Diffstat (limited to 'mod/ladspa.c')
-rw-r--r--mod/ladspa.c604
1 files changed, 604 insertions, 0 deletions
diff --git a/mod/ladspa.c b/mod/ladspa.c
new file mode 100644
index 0000000..4b681b9
--- /dev/null
+++ b/mod/ladspa.c
@@ -0,0 +1,604 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <dlfcn.h>
+#include <sys/stat.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+#include "../external/ladspa.h"
+
+#define LADSPA_TYPENAME "ladspa plugin"
+
+struct cache
+{
+ const char *key;
+ void *ptr;
+ struct cache *next;
+};
+
+struct ladspa
+{
+ ml_node *node;
+ const LADSPA_Descriptor *dsc;
+ LADSPA_Handle *hdl;
+ float *cntl;
+};
+
+static struct cache *lib_cache = NULL;
+static struct cache *dsc_cache = NULL;
+
+static void *lookup(struct cache *c, const char *k)
+{
+ while (c)
+ {
+ if (strcmp(c->key,k) == 0)
+ {
+ return c->ptr;
+ }
+ c = c->next;
+ }
+ return NULL;
+}
+
+static void cache(struct cache **c, const char *k, void *p)
+{
+ struct cache *head = calloc(1,sizeof(*head));
+ head->key = strdup(k);
+ head->ptr = p;
+ head->next = *c;
+ *c = head;
+}
+
+static int exists(const char *path)
+{
+ struct stat st;
+ return stat(path,&st) == 0;
+}
+
+static const char *find_lib(const char *path)
+{
+ if (exists(path))
+ {
+ return path;
+ }
+
+ if (strchr(path,'/') != NULL)
+ {
+ return NULL;
+ }
+
+ char *lp = getenv("LADSPA_PATH");
+
+ if (lp == NULL)
+ {
+ return NULL;
+ }
+
+ lp = strdup(lp);
+ char *ptr = lp;
+ char *dir;
+
+ while ((dir = strtok(ptr,":")) != NULL)
+ {
+ ptr = NULL;
+
+ static char buf[PATH_MAX];
+ snprintf(buf,sizeof buf,"%s/%s",dir,path);
+
+ if (exists(buf))
+ {
+ free(lp);
+ return buf;
+ }
+ }
+
+ free(lp);
+
+ return NULL;
+}
+
+static void *get_lib(lua_State *s, const char *path)
+{
+ void *lib;
+
+ if ((lib = lookup(lib_cache,path)) != NULL)
+ {
+ return lib;
+ }
+
+ const char *found = find_lib(path);
+
+ if (found == NULL)
+ {
+ luaL_error(s,"failed to find library %s",path);
+ }
+
+ lib = dlopen(found,RTLD_NOW);
+
+ if (lib == NULL)
+ {
+ luaL_error(s,"failed to load library %s: %s",dlerror());
+ }
+
+ cache(&lib_cache,path,lib);
+
+ return lib;
+}
+
+static void parse_spec(lua_State *s, const char *spec, char *path, char *name)
+{
+ char buf[128];
+ strcpy(buf,spec);
+
+ const char *f;
+
+ if ((f = strtok(buf,":")) == NULL)
+ {
+ goto invalid;
+ }
+
+ strcpy(path,f);
+
+ if ((f = strtok(NULL,":")) == NULL)
+ {
+ goto invalid;
+ }
+
+ strcpy(name,f);
+
+ return;
+
+invalid:
+ luaL_error(s,"invalid ladspa spec: %s",spec);
+}
+
+static const LADSPA_Descriptor *get_descriptor(lua_State *s, const char *spec)
+{
+ const LADSPA_Descriptor *dsc;
+ LADSPA_Descriptor_Function ldf;
+ char path[128];
+ char name[128];
+ void *lib;
+ int i = 0;
+
+ parse_spec(s,spec,path,name);
+
+ if ((dsc = lookup(dsc_cache,name)) != NULL)
+ {
+ return dsc;
+ }
+
+ lib = get_lib(s,path);
+ ldf = dlsym(lib,"ladspa_descriptor");
+
+ if (ldf == NULL)
+ {
+ luaL_error(s,"failed to load ladspa_descriptor from %s: %s",path,dlerror());
+ }
+
+ while ((dsc = ldf(i++)) != NULL)
+ {
+ if (strcmp(dsc->Name,name) == 0)
+ {
+ break;
+ }
+
+ if (strcmp(dsc->Label,name) == 0)
+ {
+ break;
+ }
+ }
+
+ if (dsc == NULL)
+ {
+ luaL_error(s,"ladspa plugin library %s does not define plugin %s",path,name);
+ }
+
+ cache(&dsc_cache,spec,(void*)dsc);
+
+ return dsc;
+}
+
+static void instantiate(struct ladspa *l, const ml_proc_args *args)
+{
+ unsigned long sr = ml_sample_rate();
+
+ l->hdl = l->dsc->instantiate(l->dsc,sr);
+
+ if (l->dsc->activate)
+ {
+ l->dsc->activate(l->hdl);
+ }
+
+ float *ptr;
+ size_t ii = 0, oi = 0, ci = 0;
+
+ for (size_t i = 0; i < l->dsc->PortCount; i++)
+ {
+ LADSPA_PortDescriptor pd = l->dsc->PortDescriptors[i];
+
+ if (LADSPA_IS_PORT_CONTROL(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ l->dsc->connect_port(l->hdl,i,&l->cntl[ci++]);
+ continue;
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ ptr = args->in[ii++];
+ l->dsc->connect_port(l->hdl,i,ptr);
+ continue;
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_OUTPUT(pd))
+ {
+ ptr = args->out[oi++];
+ l->dsc->connect_port(l->hdl,i,ptr);
+ continue;
+ }
+ }
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct ladspa *l = args->userp;
+
+ if (l->hdl == NULL)
+ {
+ instantiate(l,args);
+ }
+
+ l->dsc->run(l->hdl,args->nframes);
+}
+
+static int l_load(lua_State *s)
+{
+ const char *spec = luaL_checkstring(s,1);
+ const LADSPA_Descriptor *dsc = get_descriptor(s,spec);
+
+ struct ladspa *l = calloc(1,sizeof(*l));
+ l->dsc = dsc;
+ l->node = ml_create_node(proc,l);
+ ml_label_node(l->node,"LADSPA (%s)",spec);
+
+ size_t num_cntl = 0, arg = 2;
+ for (size_t i = 0; i < dsc->PortCount; i++)
+ {
+ LADSPA_PortDescriptor pd = dsc->PortDescriptors[i];
+
+ if (LADSPA_IS_PORT_CONTROL(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ num_cntl++;
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ ml_channel *src = mutil_get_channel(s,arg++);
+ ml_channel *dst = ml_add_input(l->node);
+ ml_connect(src,dst);
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_OUTPUT(pd))
+ {
+ ml_add_output(l->node);
+ }
+ }
+
+ l->cntl = calloc(1,sizeof(*l->cntl)*num_cntl);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = l;
+ luaL_setmetatable(s,LADSPA_TYPENAME);
+
+ return 1;
+}
+
+static int l_control(lua_State *s)
+{
+ struct ladspa *l = *(void**) luaL_checkudata(s,1,LADSPA_TYPENAME);
+ int i = luaL_checkinteger(s,2);
+ float v = luaL_checknumber(s,3);
+
+ if (i < 1 || i > l->dsc->PortCount)
+ {
+ luaL_error(s,"control port index out of bounds: %d",i);
+ }
+
+ if (!LADSPA_IS_PORT_CONTROL(l->dsc->PortDescriptors[i-1]))
+ {
+ luaL_error(s,"port %d is not a control port",i);
+ }
+
+ if (!LADSPA_IS_PORT_INPUT(l->dsc->PortDescriptors[i-1]))
+ {
+ luaL_error(s,"port %d is not an input port",i);
+ }
+
+ l->cntl[i-1] = v;
+ return 0;
+}
+
+static int l_info(lua_State *s)
+{
+ const char *spec = luaL_checkstring(s,1);
+ const LADSPA_Descriptor *dsc = get_descriptor(s,spec);
+
+ lua_newtable(s);
+ int info = lua_gettop(s);
+
+ lua_pushstring(s,dsc->Name);
+ lua_setfield(s,info,"name");
+
+ lua_pushstring(s,dsc->Label);
+ lua_setfield(s,info,"label");
+
+ lua_pushstring(s,dsc->Maker);
+ lua_setfield(s,info,"maker");
+
+ lua_pushstring(s,dsc->Copyright);
+ lua_setfield(s,info,"copyright");
+
+ lua_pushinteger(s,dsc->UniqueID);
+ lua_setfield(s,info,"id");
+
+ lua_newtable(s);
+ int ports = lua_gettop(s);
+
+ for (int i = 0; i < dsc->PortCount; i++)
+ {
+ const char *name = dsc->PortNames[i];
+ const LADSPA_PortDescriptor pd = dsc->PortDescriptors[i];
+ const LADSPA_PortRangeHint *prh = &dsc->PortRangeHints[i];
+ const LADSPA_PortRangeHintDescriptor prhd = prh->HintDescriptor;
+
+ const char *dir = LADSPA_IS_PORT_INPUT(pd) ? "input" : "output";
+ const char *type = LADSPA_IS_PORT_AUDIO(pd) ? "audio" : "control";
+ float lower = prh->LowerBound;
+ float upper = prh->UpperBound;
+ bool bounded_below = LADSPA_IS_HINT_BOUNDED_BELOW(prhd);
+ bool bounded_above = LADSPA_IS_HINT_BOUNDED_ABOVE(prhd);
+ bool toggled = LADSPA_IS_HINT_TOGGLED(prhd);
+ bool sample_rate = LADSPA_IS_HINT_SAMPLE_RATE(prhd);
+ bool logarithmic = LADSPA_IS_HINT_LOGARITHMIC(prhd);
+ bool integer = LADSPA_IS_HINT_INTEGER(prhd);
+ bool has_default = LADSPA_IS_HINT_HAS_DEFAULT(prhd);
+ bool default_minimum = LADSPA_IS_HINT_DEFAULT_MINIMUM(prhd);
+ bool default_low = LADSPA_IS_HINT_DEFAULT_LOW(prhd);
+ bool default_middle = LADSPA_IS_HINT_DEFAULT_MIDDLE(prhd);
+ bool default_high = LADSPA_IS_HINT_DEFAULT_HIGH(prhd);
+ bool default_maximum = LADSPA_IS_HINT_DEFAULT_MAXIMUM(prhd);
+ bool default_0 = LADSPA_IS_HINT_DEFAULT_0(prhd);
+ bool default_1 = LADSPA_IS_HINT_DEFAULT_1(prhd);
+ bool default_100 = LADSPA_IS_HINT_DEFAULT_100(prhd);
+ bool default_440 = LADSPA_IS_HINT_DEFAULT_440(prhd);
+
+ lua_newtable(s);
+ int port = lua_gettop(s);
+
+ lua_pushstring(s,name);
+ lua_setfield(s,port,"name");
+
+ lua_pushstring(s,dir);
+ lua_setfield(s,port,"direction");
+
+ lua_pushstring(s,type);
+ lua_setfield(s,port,"type");
+
+ lua_newtable(s);
+ int hints = lua_gettop(s);
+
+ // if (bounded_below) { lua_pushboolean(s,true); lua_setfield(s,hints,"bounded_below"); }
+ // if (bounded_above) { lua_pushboolean(s,true); lua_setfield(s,hints,"bounded_above"); }
+ if (toggled) { lua_pushboolean(s,true); lua_setfield(s,hints,"toggled"); }
+ if (sample_rate) { lua_pushboolean(s,true); lua_setfield(s,hints,"sample_rate"); }
+ if (logarithmic) { lua_pushboolean(s,true); lua_setfield(s,hints,"logarithmic"); }
+ if (integer) { lua_pushboolean(s,true); lua_setfield(s,hints,"integer"); }
+ if (bounded_below) { lua_pushnumber(s,lower); lua_setfield(s,hints,"lower_bound"); }
+ if (bounded_above) { lua_pushnumber(s,upper); lua_setfield(s,hints,"upper_bound"); }
+
+ if (has_default)
+ {
+ float def = 0;
+
+ if (default_minimum) def = lower;
+ else if (default_low) def = 0.25*lower + 0.75*upper;
+ else if (default_middle) def = 0.5*lower + 0.5*upper;
+ else if (default_high) def = 0.75*lower + 0.25*upper;
+ else if (default_maximum) def = upper;
+ else if (default_0) def = 0;
+ else if (default_1) def = 1;
+ else if (default_100) def = 100;
+ else if (default_440) def = 440;
+
+ lua_pushnumber(s,def);
+ lua_setfield(s,hints,"default");
+ }
+
+ lua_pushvalue(s,hints);
+ lua_setfield(s,port,"hints");
+
+ lua_pushvalue(s,port);
+ lua_rawseti(s,ports,i + 1);
+ lua_settop(s,ports);
+ }
+
+ lua_pushvalue(s,ports);
+ lua_setfield(s,info,"ports");
+
+ lua_settop(s,info);
+
+ return 1;
+}
+
+static int l_lib_info(lua_State *s)
+{
+ const char *path;
+ void *lib;
+ LADSPA_Descriptor_Function ldf;
+ const LADSPA_Descriptor *dsc;
+
+ path = luaL_checkstring(s,1);
+ lib= get_lib(s,path);
+ ldf = dlsym(lib,"ladspa_descriptor");
+
+ if (ldf == NULL)
+ {
+ luaL_error(s,"failed to load ladspa_descriptor from %s: %s",path,dlerror());
+ }
+
+ lua_newtable(s);
+ int info = lua_gettop(s);
+
+ int i = 0;
+ while ((dsc = ldf(i++)) != NULL)
+ {
+ lua_newtable(s);
+ int plugin = lua_gettop(s);
+
+ lua_pushstring(s,dsc->Name);
+ lua_setfield(s,plugin,"name");
+
+ lua_pushstring(s,dsc->Label);
+ lua_setfield(s,plugin,"label");
+
+ lua_pushstring(s,dsc->Maker);
+ lua_setfield(s,plugin,"maker");
+
+ lua_pushvalue(s,plugin);
+ lua_rawseti(s,info,i);
+ lua_settop(s,info);
+ }
+
+ lua_settop(s,info);
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct ladspa *l = *(void**) luaL_checkudata(s,1,LADSPA_TYPENAME);
+ size_t i = luaL_optinteger(s,2,1);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(l->node,i - 1);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_ladspa(lua_State *s)
+{
+/*** LADSPA plugin support.
+
+ local mladspa = require 'melisma.ladspa'
+
+ local spec = 'caps.so:Plate'
+ local info = mladspa.info(spec)
+
+ print('name:',info.name)
+ print('maker:',info.maker)
+
+ local input = ...
+ local plate = mladspa.load(spec,input)
+
+ plate:control(1,1)
+
+ local left = plate:out(6)
+ local right = plate:out(7)
+
+@classmod ladspa
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Load a ladspa plugin.
+
+Input signals are connected to the plugins audio input ports in the
+order that they are declared.
+
+@static
+@function load
+@tparam string spec Plugin specification.
+@tparam channel ... Input signals.
+*/
+ lua_pushcfunction(s,l_load);
+ lua_setfield(s,tbl,"load");
+
+/*** Get information about a specific plugin.
+
+Plugin specifications are written in the format `<path>:<plugin>`,
+where `<path>` is the full name of the plugin library
+(e.g. `caps.so`), and `<plugin>` is the name or label of the plugin.
+If `<path>` is not an absolute path, the library is searched for
+first in the current directory, then in `LADSPA_PATH`.
+
+This can be used to generate more ergonomic plugin interfaces.
+
+@static
+@function info
+@tparam string spec Plugin specification.
+@treturn table Plugin information.
+*/
+ lua_pushcfunction(s,l_info);
+ lua_setfield(s,tbl,"info");
+
+/*** List plugins in the specified plugin library.
+
+If `path` is not an absolute path, the library is searched for
+first in the current directory, then in `LADSPA_PATH`.
+
+@static
+@function lib_info
+@tparam string path Path to plugin library.
+@treturn table List of short plugin descriptions.
+*/
+ lua_pushcfunction(s,l_lib_info);
+ lua_setfield(s,tbl,"lib_info");
+
+/*** Set the value of an input control port.
+
+Ports are indexed based on the order they are declared by the plugin.
+This also means the indices of control ports are not necessarily consecutive.
+
+@function control
+@tparam integer i One-based port index.
+@tparam number v New control port value.
+*/
+ lua_pushcfunction(s,l_control);
+ lua_setfield(s,tbl,"control");
+
+/*** Get the audio output port at the specified index.
+
+Ports are indexed based on the order they are declared by the plugin.
+This also means the indices of audio ports are not necessarily consecutive.
+
+@function out
+@tparam integer i One-based port index.
+@treturn channel Audio output at specified index.
+*/
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,"out");
+
+ luaL_newmetatable(s,LADSPA_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}