diff options
| author | Ludvig Gunne Lindström <ludviggunnelindstrom@gmail.com> | 2026-07-17 11:43:24 +0200 |
|---|---|---|
| committer | Ludvig Gunne Lindström <ludviggunnelindstrom@gmail.com> | 2026-11-08 14:15:04 +0100 |
| commit | 039847a29868d335c08fe0a587260dffc71ca5c4 (patch) | |
| tree | e9d775090fef45ca51db576779fb373040d50e6a /mod/ladspa.c | |
| download | melisma-modules-main.tar.gz | |
Diffstat (limited to 'mod/ladspa.c')
| -rw-r--r-- | mod/ladspa.c | 604 |
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; +} |
