/* * 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 . */ #include #include #include #include #include #include #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 `:`, where `` is the full name of the plugin library (e.g. `caps.so`), and `` is the name or label of the plugin. If `` 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; }