aboutsummaryrefslogtreecommitdiff
path: root/mod/expression.c
diff options
context:
space:
mode:
Diffstat (limited to 'mod/expression.c')
-rw-r--r--mod/expression.c500
1 files changed, 500 insertions, 0 deletions
diff --git a/mod/expression.c b/mod/expression.c
new file mode 100644
index 0000000..1b78199
--- /dev/null
+++ b/mod/expression.c
@@ -0,0 +1,500 @@
+/*
+ * 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 <assert.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define EXPR_TYPENAME "expression"
+#define SCRIPT_TYPENAME "expression script"
+
+enum tag {ADD,SUB,MUL,DIV,IN,OUT,CONST,HALT};
+
+struct op
+{
+ enum tag tag;
+ float value;
+ size_t index;
+ size_t offset;
+ ml_channel *src;
+};
+
+struct script
+{
+ size_t len;
+ struct op ops[];
+};
+
+struct expr
+{
+ size_t num_inputs;
+ size_t *buf_lens;
+ size_t *cursors;
+ float **bufs;
+ size_t out_buf_len;
+ size_t out_cursor;
+ float *out_buf;
+ float *stack;
+ size_t stack_len;
+ size_t stack_p;
+ struct script *script;
+ ml_channel *out;
+};
+
+static size_t script_alloc_size(size_t len)
+{
+ return sizeof(struct script) + len*sizeof(struct op);
+}
+
+static struct script *push_script(lua_State *s, size_t len)
+{
+ struct script *script = lua_newuserdata(s,script_alloc_size(len));
+ luaL_setmetatable(s,SCRIPT_TYPENAME);
+ script->len = len;
+ return script;
+}
+
+static int l_const(lua_State *s)
+{
+ float value = luaL_checknumber(s,1);
+ struct script *script = push_script(s,1);
+ script->ops[0].tag = CONST;
+ script->ops[0].value = value;
+ return 1;
+}
+
+static int l_input(lua_State *s)
+{
+ ml_channel *src = mutil_get_channel(s,1);
+ size_t offset = luaL_optinteger(s,2,0);
+ struct script *script = push_script(s,1);
+ script->ops[0].tag = IN;
+ script->ops[0].src = src;
+ script->ops[0].offset = offset;
+ return 1;
+}
+
+static int l_output(lua_State *s)
+{
+ size_t offset = luaL_checkinteger(s,1);
+ struct script *script = push_script(s,1);
+ script->ops[0].tag = OUT;
+ script->ops[0].offset = offset;
+ return 1;
+}
+
+static int convert(lua_State *s, int i)
+{
+ if (luaL_testudata(s,i,SCRIPT_TYPENAME))
+ {
+ return i;
+ }
+
+ if (lua_isnumber(s,i))
+ {
+ lua_pushcfunction(s,l_const);
+ lua_pushvalue(s,i);
+ lua_call(s,1,1);
+ return lua_gettop(s);
+ }
+
+ const char *typename = lua_typename(s,lua_type(s,i));
+ luaL_error(s,"expected number or expression script, got %s",typename);
+
+ return 0;
+}
+
+static int binop(lua_State *s, int tag)
+{
+ int lhs_i = convert(s,1);
+ int rhs_i = convert(s,2);
+
+ struct script *lhs = luaL_checkudata(s,lhs_i,SCRIPT_TYPENAME);
+ struct script *rhs = luaL_checkudata(s,rhs_i,SCRIPT_TYPENAME);
+
+ size_t len = lhs->len + rhs->len + 1;
+ struct script *res = push_script(s,len);
+
+ memcpy(&res->ops[0],lhs->ops,sizeof(res->ops[0])*lhs->len);
+ memcpy(&res->ops[lhs->len],rhs->ops,sizeof(res->ops[0])*rhs->len);
+
+ res->ops[len-1].tag = tag;
+
+ return 1;
+}
+
+static int l_add(lua_State *s)
+{
+ return binop(s,ADD);
+}
+
+static int l_sub(lua_State *s)
+{
+ return binop(s,SUB);
+}
+
+static int l_mul(lua_State *s)
+{
+ return binop(s,MUL);
+}
+
+static int l_div(lua_State *s)
+{
+ return binop(s,DIV);
+}
+
+static void prepare_inputs(ml_node *node, struct script *script, size_t **buf_lens)
+{
+ for (size_t i = 0; i < script->len; i++)
+ {
+ struct op *op = &script->ops[i];
+
+ if (op->tag != IN)
+ {
+ continue;
+ }
+
+ /* the same input channel may have been encountered already */
+ bool have_prev = false;
+ for (size_t j = 0; j < i; j++)
+ {
+ struct op *prev = &script->ops[j];
+
+ if (prev->tag != IN)
+ {
+ continue;
+ }
+
+ if (op->src == prev->src)
+ {
+ op->index = prev->index;
+ have_prev = true;
+
+ if (op->offset+1 > (*buf_lens)[op->index])
+ {
+ /* grow lookback buffer length */
+ (*buf_lens)[op->index] = op->offset+1;
+ }
+
+ break;
+ }
+ }
+
+ if (have_prev)
+ {
+ continue;
+ }
+
+ op->index = ml_num_inputs(node);
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(op->src,dst);
+
+ /* add buffer length */
+ *buf_lens = realloc(*buf_lens,sizeof(**buf_lens)*ml_num_inputs(node));
+ (*buf_lens)[op->index] = op->offset+1;
+ }
+}
+
+static size_t max_output_offset(struct script *script)
+{
+ size_t max = 0;
+
+ for (size_t i = 0; i < script->len; i++)
+ {
+ struct op *op = &script->ops[i];
+
+ if (op->tag != OUT)
+ {
+ continue;
+ }
+
+ if (op->offset+1 > max)
+ {
+ max = op->offset+1;
+ }
+ }
+
+ return max;
+}
+
+static void proc(const ml_proc_args *args);
+
+static int l_build(lua_State *s)
+{
+ struct script *script = luaL_checkudata(s,1,SCRIPT_TYPENAME);
+
+ /* copy script and add a halt instruction */
+ struct script *copy = calloc(1,script_alloc_size(script->len+1));
+ copy->len = script->len + 1;
+ memcpy(copy->ops,script->ops,script->len*sizeof(copy->ops[0]));
+ copy->ops[script->len].tag = HALT;
+
+ struct expr *expr = calloc(1,sizeof(*expr));
+ expr->script = copy;
+
+ /* add output channel */
+ ml_node *node = ml_create_node(proc,expr);
+ ml_label_node(node,"expression");
+ expr->out = ml_add_output(node);
+
+ /* set input ops indices and determine lookback buffer lengths */
+ prepare_inputs(node,copy,&expr->buf_lens);
+ expr->num_inputs = ml_num_inputs(node);
+
+ /* allocate lookback buffers for inputs */
+ expr->bufs = calloc(1,sizeof(*expr->bufs)*expr->num_inputs);
+ expr->cursors = calloc(1,sizeof(*expr->cursors)*expr->num_inputs);
+ for (size_t i = 0; i < expr->num_inputs; i++)
+ {
+ expr->bufs[i] = calloc(1,sizeof(*expr->bufs[i])*expr->buf_lens[i]);
+ }
+
+ /* allocate lookback buffer for output */
+ expr->out_buf_len = max_output_offset(copy);
+ expr->out_buf = calloc(1,sizeof(*expr->out_buf)*expr->out_buf_len);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = expr;
+ luaL_setmetatable(s,EXPR_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct expr *e = *(void**) luaL_checkudata(s,1,EXPR_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = e->out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static void push(struct expr *expr, float v)
+{
+ if (expr->stack_p == expr->stack_len)
+ {
+ expr->stack_len = expr->stack_len == 0 ? 12 : 2*expr->stack_len;
+ expr->stack = realloc(expr->stack,sizeof(expr->stack[0])*expr->stack_len);
+ }
+
+ expr->stack[expr->stack_p++] = v;
+}
+
+static float pop(struct expr *expr)
+{
+ assert(expr->stack_p > 0);
+ return expr->stack[--expr->stack_p];
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct expr *e = args->userp;
+
+ static const void *tbl[] = {
+ [ADD] = &&op_add,
+ [SUB] = &&op_sub,
+ [MUL] = &&op_mul,
+ [DIV] = &&op_div,
+ [IN] = &&op_in,
+ [OUT] = &&op_out,
+ [CONST] = &&op_const,
+ [HALT] = &&op_halt,
+ };
+
+ float *out = args->out[0];
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ /* update lookback buffers */
+ for (size_t j = 0; j < e->num_inputs; j++)
+ {
+ e->bufs[j][e->cursors[j]] = args->in[j][i];
+ }
+
+ /* execute script */
+ size_t pc = 0;
+ struct op *op;
+ float rhs,lhs,res;
+ size_t l;
+
+ #define NEXT()\
+ op = &e->script->ops[pc++];\
+ goto *tbl[op->tag];
+
+ NEXT();
+
+ op_add:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs+rhs);
+ NEXT();
+
+ op_sub:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs-rhs);
+ NEXT();
+
+ op_mul:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs*rhs);
+ NEXT();
+
+ op_div:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs/rhs);
+ NEXT();
+
+ op_in:
+ l = e->cursors[op->index] + e->buf_lens[op->index] - op->offset;
+ while (l >= e->buf_lens[op->index])
+ {
+ l -= e->buf_lens[op->index];
+ }
+ push(e,e->bufs[op->index][l]);
+ NEXT();
+
+ op_out:
+ l = e->out_cursor + e->out_buf_len - op->offset;
+ while (l >= e->out_buf_len)
+ {
+ l -= e->out_buf_len;
+ }
+ push(e,e->out_buf[l]);
+ NEXT();
+
+ op_const:
+ push(e,op->value);
+ NEXT();
+
+ op_halt:
+ assert(e->stack_p == 1);
+ out[i] = pop(e);
+
+ /* update output lookback buffer */
+ if (e->out_buf_len > 0)
+ {
+ e->out_buf[e->out_cursor++] = out[i];
+ if (e->out_cursor == e->out_buf_len)
+ {
+ e->out_cursor = 0;
+ }
+ }
+
+ /* update input looback buffer cursors */
+ for (size_t j = 0; j < e->num_inputs; j++)
+ {
+ e->cursors[j]++;
+ if (e->cursors[j] == e->buf_lens[j])
+ {
+ e->cursors[j] = 0;
+ }
+ }
+ }
+}
+
+int luaopen_melisma_expression(lua_State *s)
+{
+/*** Build filters with arithmetic expressions.
+
+ local mexpression = require 'melisma.expression'
+ local input = mepxression.input
+
+ local lhs = ...
+ local rhs = ...
+
+ local script = input(lhs,0) + 0.5 * input(rhs,10)
+ local expression = script:build()
+
+@module expression
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushcfunction(s,l_const);
+ lua_setfield(s,tbl,"const");
+
+/*** Introduce an input signal in the expression.
+@static
+@function input
+@tparam channel sig Input signal.
+@tparam integer off Offset in no. of samples.
+@treturn script
+*/
+ lua_pushcfunction(s,l_input);
+ lua_setfield(s,tbl,"input");
+
+
+/*** Introduce the output signal in the expression.
+@static
+@function output
+@tparam integer off Offset in no. of samples.
+@treturn script
+*/
+ lua_pushcfunction(s,l_output);
+ lua_setfield(s,tbl,"output");
+
+/*** Represents an expression before compilation.
+Implements `__add`, `__sub`, `__mul` and `__div` for
+combining scripts in to larger ones.
+@type script
+*/
+/*** Compile an expression script.
+@function script:build
+@treturn expression
+*/
+ lua_pushcfunction(s,l_build);
+ lua_setfield(s,tbl,"build");
+
+ luaL_newmetatable(s,SCRIPT_TYPENAME);
+ int script_mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,script_mt,"__index");
+
+ lua_pushcfunction(s,l_add);
+ lua_setfield(s,script_mt,"__add");
+
+ lua_pushcfunction(s,l_sub);
+ lua_setfield(s,script_mt,"__sub");
+
+ lua_pushcfunction(s,l_mul);
+ lua_setfield(s,script_mt,"__mul");
+
+ lua_pushcfunction(s,l_div);
+ lua_setfield(s,script_mt,"__div");
+
+ lua_newtable(s);
+ int expr_tbl = lua_gettop(s);
+
+ luaL_newmetatable(s,EXPR_TYPENAME);
+ int expr_mt = lua_gettop(s);
+
+ lua_pushvalue(s,expr_tbl);
+ lua_setfield(s,expr_mt,"__index");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,expr_tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ lua_settop(s,tbl);
+ return 1;
+}