/* * 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 "graph.h" #include "mem.h" #ifndef BUFFER_SIZE #define BUFFER_SIZE 4096 #endif struct ml_channel { ml_node *node; enum {IN,OUT} dir; ml_channel *src; ml_channel *next; size_t buf; int refs; }; struct ml_node { ml_proc_fn proc; void *userp; ml_channel *chans[2]; ml_node *next; bool visited; bool cycle; char *label; }; struct chunk { char *label; ml_proc_fn proc; ml_proc_args args; struct chunk *next; float *data[]; }; struct free_list { size_t buf; struct free_list *next; }; static ml_node *root; static ml_node *active; static ml_node *active_tail; static struct free_list *free_list; static size_t counter; static float *buffers; static struct chunk *graph; ml_node *ml_create_node(ml_proc_fn proc, void *userp) { ml_node *node = scalloc(1,sizeof(*node)); node->proc = proc; node->userp = userp; return node; } void ml_label_node(ml_node *node, const char *fmt, ...) { va_list ap_alloc,ap_print; va_start(ap_alloc,fmt); va_copy(ap_print,ap_alloc); size_t len = 1 + vsnprintf(NULL,0,fmt,ap_alloc); va_end(ap_alloc); char *buf = smalloc(len); vsnprintf(buf,len,fmt,ap_print); node->label = buf; } void *ml_get_userp(ml_node *node) { return node->userp; } ml_channel *add_channel(ml_node *node, int dir) { ml_channel *chan = scalloc(1,sizeof(*chan)); chan->dir = dir; chan->node = node; if (node->chans[dir] == NULL) { node->chans[dir] = chan; } else { ml_channel *insert = node->chans[dir]; while (insert->next) { insert = insert->next; } insert->next = chan; } return chan; } ml_channel *ml_add_input(ml_node *node) { return add_channel(node,IN); } ml_channel *ml_add_output(ml_node *node) { return add_channel(node,OUT); } static ml_channel *get_channel(ml_node *node, size_t i, int dir) { ml_channel *chan = node->chans[dir]; while (i > 0) { if (chan == NULL) { break; } i--; chan = chan->next; } return chan; } ml_channel *ml_get_input(ml_node *node, size_t i) { return get_channel(node,i,IN); } ml_channel *ml_get_output(ml_node *node, size_t i) { return get_channel(node,i,OUT); } static size_t num_channels(ml_node *node, int dir) { ml_channel *chan = node->chans[dir]; size_t n = 0; while (chan != NULL) { n++; chan = chan->next; } return n; } size_t ml_num_inputs(ml_node *node) { return num_channels(node,IN); } size_t ml_num_outputs(ml_node *node) { return num_channels(node,OUT); } void ml_connect(ml_channel *src, ml_channel *dst) { assert(src->dir == OUT); assert(dst->dir == IN); src->refs++; dst->src = src; } ml_node *ml_get_root_node(void) { return root; } void init_graph(lua_State *s) { root = ml_create_node(NULL,NULL); ml_label_node(root,"root"); luaL_newmetatable(s,MELISMA_NODE_TYPENAME); luaL_newmetatable(s,MELISMA_INPUT_CHANNEL_TYPENAME); luaL_newmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); } static const char *topo_sort(ml_node *node) { if (node->visited) { return NULL; } if (node->cycle) { return "cycle in audio graph detected"; } node->visited = true; node->cycle = true; ml_channel *in = node->chans[IN]; while (in != NULL) { if (in->src == NULL) { return "disconnected input channel in audio graph detected"; } const char *err = topo_sort(in->src->node); if (err != NULL) { return err; } in = in->next; } if (active_tail) { active_tail->next = node; } else { active = node; } active_tail = node; node->cycle = false; return NULL; } static size_t alloc_buf(void) { if (free_list) { struct free_list *head = free_list; size_t buf = head->buf; free_list = head->next; free(head); return buf; } return counter++; } static void free_buf(size_t buf) { struct free_list *head = scalloc(1,sizeof(*head)); head->buf = buf; head->next = free_list; free_list = head; } static void assign_bufs() { ml_node *node = active; while (node) { ml_channel *in, *out; in = node->chans[IN]; out = node->chans[OUT]; while (out) { out->buf = alloc_buf(); out = out->next; } while (in) { if (--in->src->refs == 0) { free_buf(in->src->buf); } in = in->next; } node = node->next; } } static void make_chunks(void) { struct chunk *chunk, *tail = NULL; struct ml_node *node = active; while (node) { size_t icnt = ml_num_inputs(node); size_t ocnt = ml_num_outputs(node); size_t nchans = icnt + ocnt; size_t size = sizeof(*chunk) + sizeof(chunk->data[0]) * nchans; chunk = scalloc(1,size); chunk->label = node->label ? strdup(node->label) : NULL; chunk->proc = node->proc; chunk->args.userp = node->userp; chunk->args.icnt = icnt; chunk->args.ocnt = ocnt; chunk->args.in = chunk->data; chunk->args.out = chunk->data + icnt; ml_channel *in, *out; size_t i = 0; in = node->chans[IN]; while (in != NULL) { chunk->data[i++] = &buffers[BUFFER_SIZE*in->src->buf]; in = in->next; } out = node->chans[OUT]; while (out != NULL) { chunk->data[i++] = &buffers[BUFFER_SIZE*out->buf]; out = out->next; } assert(i == nchans); if (tail == NULL) { graph = chunk; } else { tail->next = chunk; } tail = chunk; node = node->next; } } const char *build_graph(void) { const char *err = topo_sort(root); if (err != NULL) { return err; } assign_bufs(); buffers = malloc(sizeof(*buffers)*BUFFER_SIZE*counter); make_chunks(); return NULL; } static void proc_once(size_t n) { struct chunk *c = graph; while (c) { if (c->proc) { c->args.nframes = n; c->proc(&c->args); } c = c->next; } } void proc_graph(size_t n) { while (n > 0) { size_t m = n > BUFFER_SIZE ? BUFFER_SIZE : n; proc_once(m); n -= m; } } static void add_to_layout(lua_State *s, int layout, ml_node *node) { int top = lua_absindex(s,lua_gettop(s)); lua_pushlightuserdata(s,node); lua_gettable(s,layout); if (!lua_isnil(s,-1)) { return; } lua_newtable(s); int tbl = lua_absindex(s,lua_gettop(s)); if (node->label) { lua_pushstring(s,node->label); lua_setfield(s,tbl,"label"); } lua_newtable(s); int list = lua_absindex(s,lua_gettop(s)); ml_channel *in = node->chans[IN]; int i = 1; while (in) { add_to_layout(s,layout,in->src->node); lua_pushlightuserdata(s,in->src->node); lua_rawseti(s,list,i++); in = in->next; } lua_pushvalue(s,list); lua_setfield(s,tbl,"inputs"); lua_pushlightuserdata(s,node); lua_pushvalue(s,tbl); lua_settable(s,layout); lua_settop(s,top); } int l_graph_layout(lua_State *s) { lua_newtable(s); int layout = lua_absindex(s,lua_gettop(s)); add_to_layout(s,layout,root); lua_pushvalue(s,layout); return 1; }