/* * 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 "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; }