aboutsummaryrefslogtreecommitdiff
path: root/src/port.c
diff options
context:
space:
mode:
authorLudvig Gunne Lindström <ludviggunnelindstrom@gmail.com>2026-07-17 11:42:57 +0200
committerLudvig Gunne Lindström <ludviggunnelindstrom@gmail.com>2026-11-08 14:07:53 +0100
commit98b3c66ebc79597f5b54fff132eaafbebf581248 (patch)
treef999ec466ae3ac635653d1fe0a6a6dd1a2f2c3b0 /src/port.c
downloadmelisma-98b3c66ebc79597f5b54fff132eaafbebf581248.tar.gz
Diffstat (limited to 'src/port.c')
-rw-r--r--src/port.c546
1 files changed, 546 insertions, 0 deletions
diff --git a/src/port.c b/src/port.c
new file mode 100644
index 0000000..fb487e3
--- /dev/null
+++ b/src/port.c
@@ -0,0 +1,546 @@
+/*
+ * 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 <string.h>
+#include <assert.h>
+
+#include <lua.h>
+#include <lauxlib.h>
+
+#include <jack/jack.h>
+#include <jack/midiport.h>
+
+#include "mem.h"
+#include "port.h"
+#include "graph.h"
+#include "event.h"
+
+extern jack_client_t *client;
+
+enum port_type
+{
+ AUDIO_INPUT,
+ MIDI_INPUT,
+ AUDIO_OUTPUT,
+ MIDI_OUTPUT,
+};
+
+struct port
+{
+ enum port_type type;
+ jack_port_t *jack_port;
+ void *jack_buf;
+ int fn;
+ ml_channel *chan;
+ struct port *next;
+};
+
+struct port_descriptor
+{
+ const char *const lua_typename;
+ const char *const jack_typename;
+ const unsigned long jack_flags;
+ const char *const peer_jack_typename;
+ const unsigned long peer_jack_flags;
+ struct port *list;
+};
+
+static struct port_descriptor descriptors[] = {
+ [AUDIO_INPUT] = {
+ .lua_typename = "audio input port",
+ .jack_typename = JACK_DEFAULT_AUDIO_TYPE,
+ .jack_flags = JackPortIsInput,
+ .peer_jack_typename = JACK_DEFAULT_AUDIO_TYPE,
+ .peer_jack_flags = JackPortIsOutput,
+ },
+ [MIDI_INPUT] = {
+ .lua_typename = "midi input port",
+ .jack_typename = JACK_DEFAULT_MIDI_TYPE,
+ .jack_flags = JackPortIsInput,
+ .peer_jack_typename = JACK_DEFAULT_MIDI_TYPE,
+ .peer_jack_flags = JackPortIsOutput,
+ },
+ [AUDIO_OUTPUT] = {
+ .lua_typename = "audio output port",
+ .jack_typename = JACK_DEFAULT_AUDIO_TYPE,
+ .jack_flags = JackPortIsOutput,
+ .peer_jack_typename = JACK_DEFAULT_AUDIO_TYPE,
+ .peer_jack_flags = JackPortIsInput,
+ },
+ [MIDI_OUTPUT] = {
+ .lua_typename = "midi output port",
+ .jack_typename = JACK_DEFAULT_MIDI_TYPE,
+ .jack_flags = JackPortIsOutput,
+ .peer_jack_typename = JACK_DEFAULT_MIDI_TYPE,
+ .peer_jack_flags = JackPortIsInput,
+ },
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct port *port = args->userp;
+
+ switch (port->type)
+ {
+ case AUDIO_INPUT:
+ memcpy(args->out[0],
+ port->jack_buf,
+ sizeof(*args->out[0])*args->nframes);
+ port->jack_buf = args->nframes + (float*)port->jack_buf;
+ break;
+
+ case AUDIO_OUTPUT:
+ memcpy(port->jack_buf,
+ args->in[0],
+ sizeof(*args->in[0])*args->nframes);
+ port->jack_buf = args->nframes + (float*)port->jack_buf;
+ break;
+
+ case MIDI_INPUT:
+ case MIDI_OUTPUT:
+ assert(0);
+ }
+}
+
+// TODO: Use mutil.h for this when moving to a module
+static ml_channel *get_channel(lua_State *s, int i)
+{
+ ml_channel **c;
+
+ if ((c = luaL_testudata(s,i,MELISMA_OUTPUT_CHANNEL_TYPENAME)) != NULL)
+ {
+ return *c;
+ }
+
+ lua_getfield(s,i,"__ml_out");
+
+ if (!lua_isfunction(s,-1))
+ {
+ lua_pop(s,1);
+ goto error;
+ }
+
+ lua_pushvalue(s,i);
+ lua_call(s,1,1);
+
+ if ((c = luaL_testudata(s,-1,MELISMA_OUTPUT_CHANNEL_TYPENAME)) != NULL)
+ {
+ lua_pop(s,1);
+ return *c;
+ }
+
+ lua_pop(s,1);
+
+error:
+ luaL_argerror(s,i,"expected output channel");
+ return NULL;
+}
+
+static int l_open(lua_State *s)
+{
+ int type_i = lua_upvalueindex(1);
+ enum port_type type = lua_tointeger(s,type_i);
+ struct port_descriptor *pd = &descriptors[type];
+ const char *name = luaL_checkstring(s,1);
+
+ ml_channel *src = NULL;
+
+ if (type == AUDIO_OUTPUT)
+ {
+ src = get_channel(s,2);
+ }
+
+ jack_port_t *jack_port = jack_port_register(client,name,pd->jack_typename,pd->jack_flags,0);
+
+ if (jack_port == NULL)
+ {
+ luaL_error(s,"failed to register port '%s'",name);
+ }
+
+ struct port *port = scalloc(1,sizeof(*port));
+ port->type = type;
+ port->jack_port = jack_port;
+ port->jack_buf = NULL;
+ port->fn = LUA_NOREF;
+ port->next = pd->list;
+ pd->list = port;
+
+ switch (type)
+ {
+ case AUDIO_INPUT:
+ {
+ ml_node *node = ml_create_node(proc,port);
+ port->chan = ml_add_output(node);
+ }
+ break;
+ break;
+
+ case MIDI_INPUT:
+ {
+ }
+ break;
+
+ case AUDIO_OUTPUT:
+ {
+ ml_node *node = ml_create_node(proc,port);
+ ml_channel *out = ml_add_output(node);
+ ml_channel *in = ml_add_input(node);
+ ml_connect(src,in);
+ ml_connect(out,ml_add_input(ml_get_root_node()));
+ }
+ break;
+
+ case MIDI_OUTPUT:
+ {
+ }
+ break;
+ }
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = port;
+ luaL_setmetatable(s,pd->lua_typename);
+
+ return 1;
+}
+
+static int l_connect(lua_State *s)
+{
+ int type_i = lua_upvalueindex(1);
+ enum port_type type = lua_tointeger(s,type_i);
+ struct port_descriptor *pd = &descriptors[type];
+ struct port *port = *(void**) luaL_checkudata(s,1,pd->lua_typename);
+
+ const char *dst = luaL_checkstring(s,2);
+ const char *src = jack_port_name(port->jack_port);
+
+ if (type == AUDIO_INPUT || type == MIDI_INPUT)
+ {
+ const char *tmp = dst;
+ dst = src;
+ src = tmp;
+ }
+
+ if (jack_connect(client,src,dst) != 0)
+ {
+ luaL_error(s,"failed to connect ports '%s' and '%s'",src,dst);
+ }
+
+ return 0;
+}
+
+static int l_peers(lua_State *s)
+{
+ int type_i = lua_upvalueindex(1);
+ enum port_type type = lua_tointeger(s,type_i);
+ struct port_descriptor *pd = &descriptors[type];
+
+ const char *pattern = luaL_optstring(s,1,"");
+
+ const char *typename = pd->peer_jack_typename;
+ unsigned long flags = pd->peer_jack_flags;
+
+ const char **list = jack_get_ports(client,pattern,typename,flags);
+
+ if (list == NULL)
+ {
+ luaL_error(s,"failed to list ports");
+ }
+
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ for (int i = 0; list[i] != NULL; i++)
+ {
+ const char *name = list[i];
+
+ jack_port_t *handle = jack_port_by_name(client,name);
+ if (jack_port_is_mine(client,handle))
+ {
+ continue;
+ }
+
+ lua_pushstring(s,list[i]);
+ lua_rawseti(s,tbl,i + 1);
+ }
+
+ jack_free(list);
+
+ lua_settop(s,tbl);
+ return 1;
+}
+
+static int l_callback(lua_State *s)
+{
+ struct port *port = *(void**) luaL_checkudata(s,1,descriptors[MIDI_INPUT].lua_typename);
+ luaL_checktype(s,2,LUA_TFUNCTION);
+ lua_pushvalue(s,2);
+ port->fn = luaL_ref(s,LUA_REGISTRYINDEX);
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct port *port = *(void**) luaL_checkudata(s,1,descriptors[AUDIO_INPUT].lua_typename);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = port->chan;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static int load_ports(lua_State *s)
+{
+ lua_newtable(s);
+ int root = lua_gettop(s);
+
+ {
+ /* AUDIO */
+ lua_newtable(s);
+ int audio = lua_gettop(s);
+
+ lua_pushinteger(s,AUDIO_OUTPUT);
+ lua_pushcclosure(s,l_peers,1);
+ lua_setfield(s,audio,"destinations");
+
+ lua_pushinteger(s,AUDIO_INPUT);
+ lua_pushcclosure(s,l_peers,1);
+ lua_setfield(s,audio,"sources");
+
+ /* AUDIO INPUT */
+ {
+ lua_newtable(s);
+ int audio_input = lua_gettop(s);
+
+ // TODO: Use mutil.h
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,audio_input,"__ml_out");
+
+ luaL_newmetatable(s,descriptors[AUDIO_INPUT].lua_typename);
+ int audio_input_mt = lua_gettop(s);
+
+ lua_pushvalue(s,audio_input);
+ lua_setfield(s,audio_input_mt,"__index");
+
+ lua_pushinteger(s,AUDIO_INPUT);
+ lua_pushcclosure(s,l_open,1);
+ lua_setfield(s,audio_input,"open");
+
+ lua_pushinteger(s,AUDIO_INPUT);
+ lua_pushcclosure(s,l_connect,1);
+ lua_setfield(s,audio_input,"connect");
+
+ lua_pushvalue(s,audio_input);
+ lua_setfield(s,audio,"input");
+ }
+
+ /* AUDIO OUTPUT */
+ {
+ lua_newtable(s);
+ int audio_output = lua_gettop(s);
+
+ luaL_newmetatable(s,descriptors[AUDIO_OUTPUT].lua_typename);
+ int audio_output_mt = lua_gettop(s);
+
+ lua_pushvalue(s,audio_output);
+ lua_setfield(s,audio_output_mt,"__index");
+
+ lua_pushinteger(s,AUDIO_OUTPUT);
+ lua_pushcclosure(s,l_open,1);
+ lua_setfield(s,audio_output,"open");
+
+ lua_pushinteger(s,AUDIO_OUTPUT);
+ lua_pushcclosure(s,l_connect,1);
+ lua_setfield(s,audio_output,"connect");
+
+ lua_pushvalue(s,audio_output);
+ lua_setfield(s,audio,"output");
+ }
+
+ lua_pushvalue(s,audio);
+ lua_setfield(s,root,"audio");
+ }
+
+ /* MIDI */
+ {
+ lua_newtable(s);
+ int midi = lua_gettop(s);
+
+ lua_pushinteger(s,MIDI_OUTPUT);
+ lua_pushcclosure(s,l_peers,1);
+ lua_setfield(s,midi,"destinations");
+
+ lua_pushinteger(s,MIDI_INPUT);
+ lua_pushcclosure(s,l_peers,1);
+ lua_setfield(s,midi,"sources");
+
+ /* MIDI INPUT */
+ {
+ lua_newtable(s);
+ int midi_input = lua_gettop(s);
+
+ luaL_newmetatable(s,descriptors[MIDI_INPUT].lua_typename);
+ int midi_input_mt = lua_gettop(s);
+
+ lua_pushvalue(s,midi_input);
+ lua_setfield(s,midi_input_mt,"__index");
+
+ lua_pushinteger(s,MIDI_INPUT);
+ lua_pushcclosure(s,l_open,1);
+ lua_setfield(s,midi_input,"open");
+
+ lua_pushinteger(s,MIDI_INPUT);
+ lua_pushcclosure(s,l_connect,1);
+ lua_setfield(s,midi_input,"connect");
+
+ lua_pushinteger(s,MIDI_INPUT);
+ lua_pushcclosure(s,l_callback,1);
+ lua_setfield(s,midi_input,"callback");
+
+ lua_pushvalue(s,midi_input);
+ lua_setfield(s,midi,"input");
+ }
+
+ /* MIDI OUTPUT */
+ {
+ lua_newtable(s);
+ int midi_output = lua_gettop(s);
+
+ luaL_newmetatable(s,descriptors[MIDI_OUTPUT].lua_typename);
+ int midi_output_mt = lua_gettop(s);
+
+ lua_pushvalue(s,midi_output);
+ lua_setfield(s,midi_output_mt,"__index");
+
+ lua_pushinteger(s,MIDI_OUTPUT);
+ lua_pushcclosure(s,l_open,1);
+ lua_setfield(s,midi_output,"open");
+
+ lua_pushinteger(s,MIDI_OUTPUT);
+ lua_pushcclosure(s,l_connect,1);
+ lua_setfield(s,midi_output,"connect");
+
+ lua_pushvalue(s,midi_output);
+ lua_setfield(s,midi,"output");
+ }
+
+ lua_pushvalue(s,midi);
+ lua_setfield(s,root,"midi");
+ }
+
+ lua_settop(s,root);
+ return 1;
+}
+
+static void set_jack_buffers(enum port_type type, size_t n)
+{
+ struct port *port = descriptors[type].list;
+ while (port)
+ {
+ port->jack_buf = jack_port_get_buffer(port->jack_port,n);
+ port = port->next;
+ }
+}
+
+static void schedule_midi_event(unsigned long t, int fn, int status, int data1, int data2)
+{
+ extern lua_State *state;
+ lua_newtable(state);
+ int tbl = lua_absindex(state,lua_gettop(state));
+
+ lua_pushinteger(state,status);
+ lua_setfield(state,tbl,"status");
+
+ lua_pushinteger(state,data1);
+ lua_setfield(state,tbl,"data1");
+
+ lua_pushinteger(state,data2);
+ lua_setfield(state,tbl,"data2");
+
+ int arg = luaL_ref(state,LUA_REGISTRYINDEX);
+ ml_schedule_event(t,fn,arg);
+}
+
+static void flush_midi_events(void)
+{
+ struct port *port = descriptors[MIDI_INPUT].list;
+ for (; port; port = port->next)
+ {
+ if (port->fn == LUA_NOREF)
+ {
+ continue;
+ }
+
+ size_t n = jack_midi_get_event_count(port->jack_buf);
+ for (size_t i = 0; i < n; i++)
+ {
+ jack_midi_event_t e;
+ jack_midi_event_get(&e,port->jack_buf,i);
+ schedule_midi_event(e.time,port->fn,e.buffer[0],e.buffer[1],e.buffer[2]);
+ }
+ }
+}
+
+static int process_callback(jack_nframes_t n, void *args)
+{
+ set_jack_buffers(AUDIO_INPUT,n);
+ set_jack_buffers(MIDI_INPUT,n);
+ set_jack_buffers(AUDIO_OUTPUT,n);
+ set_jack_buffers(MIDI_OUTPUT,n);
+
+ unsigned long t0 = jack_last_frame_time(client);
+ unsigned long t = 0;
+ unsigned long dt = 0;
+ ml_event_id id;
+
+ flush_midi_events();
+
+ while ((id = next_event(n - t, &dt)) != NO_EVENT)
+ {
+ proc_graph(dt);
+ dispatch_event(id);
+
+ t += dt;
+ t0 += dt;
+ }
+
+ proc_graph(dt);
+
+ return 0;
+}
+
+static int xrun_callback(void *args)
+{
+ fprintf(stderr,"XRUN\n");
+ return 0;
+}
+
+void init_ports(lua_State *s)
+{
+ int top = lua_gettop(s);
+
+ lua_getglobal(s,"package");
+ int package = lua_gettop(s);
+
+ lua_getfield(s,package,"preload");
+ int preload = lua_gettop(s);
+
+ lua_pushcfunction(s,load_ports);
+ lua_setfield(s,preload,"melisma.port");
+
+ lua_settop(s,top);
+
+ jack_set_process_callback(client,process_callback,NULL);
+ jack_set_xrun_callback(client,xrun_callback,NULL);
+}