From 039847a29868d335c08fe0a587260dffc71ca5c4 Mon Sep 17 00:00:00 2001 From: Ludvig Gunne Lindström Date: Fri, 17 Jul 2026 11:43:24 +0200 Subject: init --- mod/biquad.c | 202 ++++++++++++++++++ mod/buffer.c | 281 +++++++++++++++++++++++++ mod/delay.c | 233 +++++++++++++++++++++ mod/expression.c | 500 ++++++++++++++++++++++++++++++++++++++++++++ mod/fs.c | 103 ++++++++++ mod/gain.c | 164 +++++++++++++++ mod/interp.c | 257 +++++++++++++++++++++++ mod/keycallback.c | 147 +++++++++++++ mod/ladspa.c | 604 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ mod/lookback.c | 106 ++++++++++ mod/map.h | 49 +++++ mod/mixer.c | 114 +++++++++++ mod/mutil.h | 69 +++++++ mod/null.c | 76 +++++++ mod/nullsink.c | 111 ++++++++++ mod/oscil.c | 438 +++++++++++++++++++++++++++++++++++++++ mod/pan.c | 187 +++++++++++++++++ mod/passthrough.c | 82 ++++++++ mod/playback.c | 342 +++++++++++++++++++++++++++++++ mod/sample.c | 180 ++++++++++++++++ mod/sample.h | 33 +++ mod/serial.c | 511 +++++++++++++++++++++++++++++++++++++++++++++ mod/spring.c | 173 ++++++++++++++++ mod/timer.c | 152 ++++++++++++++ mod/udp.c | 391 +++++++++++++++++++++++++++++++++++ 25 files changed, 5505 insertions(+) create mode 100644 mod/biquad.c create mode 100644 mod/buffer.c create mode 100644 mod/delay.c create mode 100644 mod/expression.c create mode 100644 mod/fs.c create mode 100644 mod/gain.c create mode 100644 mod/interp.c create mode 100644 mod/keycallback.c create mode 100644 mod/ladspa.c create mode 100644 mod/lookback.c create mode 100644 mod/map.h create mode 100644 mod/mixer.c create mode 100644 mod/mutil.h create mode 100644 mod/null.c create mode 100644 mod/nullsink.c create mode 100644 mod/oscil.c create mode 100644 mod/pan.c create mode 100644 mod/passthrough.c create mode 100644 mod/playback.c create mode 100644 mod/sample.c create mode 100644 mod/sample.h create mode 100644 mod/serial.c create mode 100644 mod/spring.c create mode 100644 mod/timer.c create mode 100644 mod/udp.c (limited to 'mod') diff --git a/mod/biquad.c b/mod/biquad.c new file mode 100644 index 0000000..6591d87 --- /dev/null +++ b/mod/biquad.c @@ -0,0 +1,202 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define LOWPASS_TYPENAME "biquad LPF" +#define LOWPASS_DEFAULT_CUTOFF 4000 +#define LOWPASS_DEFAULT_RESONANCE 0.5 + +struct biquad; + +typedef void (*recomp_fn)(struct biquad*); + +struct biquad +{ + ml_channel *out; + recomp_fn recomp; + double x_1,x_2,y_1,y_2; + double xc_0,xc_1,xc_2; + double yc_1,yc_2; + double Fs; + double f0; + double Q; +}; + +static inline void lowpass_recomp(struct biquad *bq) +{ + double w0,cosw0,sinw0,alpha,b0,b1,b2,a0,a1,a2; + w0 = 2*M_PI*bq->f0/bq->Fs; + cosw0 = cos(w0); + sinw0 = sin(w0); + alpha = sinw0/(2*bq->Q); + b0 = (1 - cosw0)/2; + b1 = 1 - cosw0; + b2 = (1 - cosw0)/2; + a0 = 1 + alpha; + a1 = -2*cosw0; + a2 = 1 - alpha; + bq->xc_0 = b0/a0; + bq->xc_1 = b1/a0; + bq->xc_2 = b2/a0; + bq->yc_1 = a1/a0; + bq->yc_2 = a2/a0; +} + +static void proc(const ml_proc_args *args) +{ + struct biquad bq = *(struct biquad*) args->userp; + + float *x = args->in[0]; + float *y = args->out[0]; + + for (size_t i = 0; i < args->nframes; i++) + { + y[i] = bq.xc_0*x[i] + + bq.xc_1*bq.x_1 + + bq.xc_2*bq.x_2 + - bq.yc_1*bq.y_1 + - bq.yc_2*bq.y_2; + + bq.x_2 = bq.x_1; + bq.x_1 = x[i]; + bq.y_2 = bq.y_1; + bq.y_1 = y[i]; + } + + *(struct biquad*) args->userp = bq; +} + +static int l_lowpass_init(lua_State *s) +{ + ml_channel *src = mutil_get_channel(s,1); + + struct biquad *bq = calloc(1,sizeof(*bq)); + bq->recomp = lowpass_recomp; + bq->Fs = ml_sample_rate(); + bq->f0 = LOWPASS_DEFAULT_CUTOFF; + bq->Q = LOWPASS_DEFAULT_RESONANCE; + bq->recomp(bq); + + ml_node *node = ml_create_node(proc,bq); + ml_label_node(node,"lowpass"); + + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + + bq->out = ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = bq; + luaL_setmetatable(s,LOWPASS_TYPENAME); + + return 1; +} + +static int l_lowpass_resonance(lua_State *s) +{ + struct biquad *bq = *(void**) luaL_checkudata(s,1,LOWPASS_TYPENAME); + bq->Q = luaL_checknumber(s,2); + bq->recomp(bq); + *(void**) lua_newuserdata(s,sizeof(void*)) = bq; + luaL_setmetatable(s,LOWPASS_TYPENAME); + return 1; +} + +static int l_lowpass_cutoff(lua_State *s) +{ + struct biquad *bq = *(void**) luaL_checkudata(s,1,LOWPASS_TYPENAME); + bq->f0 = luaL_checknumber(s,2); + bq->recomp(bq); + *(void**) lua_newuserdata(s,sizeof(void*)) = bq; + luaL_setmetatable(s,LOWPASS_TYPENAME); + return 1; +} + +static int l_lowpass_out(lua_State *s) +{ + struct biquad *bq = *(void**) luaL_checkudata(s,1,LOWPASS_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = bq->out; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_biquad(lua_State *s) +{ +/*** Digital biquad filters. +@module biquad +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Lowpass filter. +@type biquad.lowpass +@usage + + local mbiquad = require 'melisma.biquad' + local input = ... + local lowpass = mbiquad.lowpass.init(node, 2000, 0.6) + lowpass:resonance(1000) + lowpass:cutoff(0.25) +*/ + lua_newtable(s); + int lowpass_tbl = lua_gettop(s); + +/*** Create a lowpass filter. +@static +@function init +@tparam channel input Input signal. +@treturn biquad.lowpass The newly created lowpass filter. +*/ + lua_pushcfunction(s,l_lowpass_init); + lua_setfield(s,lowpass_tbl,"init"); + +/*** Set filter resonance. +@function lowpass:resonance +@tparam number res Resonance. +*/ + lua_pushcfunction(s,l_lowpass_resonance); + lua_setfield(s,lowpass_tbl,"resonance"); + + +/*** Set filter cutoff frequency. +@function lowpass:cutoff +@tparam number cut Cutoff frequency. +*/ + lua_pushcfunction(s,l_lowpass_cutoff); + lua_setfield(s,lowpass_tbl,"cutoff"); + + lua_pushcfunction(s,l_lowpass_out); + lua_setfield(s,lowpass_tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,LOWPASS_TYPENAME); + int lowpass_mt = lua_gettop(s); + + lua_pushvalue(s,lowpass_tbl); + lua_setfield(s,lowpass_mt,"__index"); + + lua_pushvalue(s,lowpass_tbl); + lua_setfield(s,tbl,"lowpass"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/buffer.c b/mod/buffer.c new file mode 100644 index 0000000..64ba13e --- /dev/null +++ b/mod/buffer.c @@ -0,0 +1,281 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define BUFFER_TYPENAME "buffer" + +enum mode {CAPTURE,PLAYBACK,IDLE}; + +struct buffer +{ + ml_channel *chan; + enum mode mode; + double phase; + double speed; + size_t len; + size_t cap; + float data[]; +}; + +static size_t capture(struct buffer *b, const float *in, size_t n) +{ + size_t i = 0; + + for (; i < n; i++) + { + b->data[b->len++] = in[i]; + + if (b->len == b->cap) + { + b->mode = IDLE; + b->phase = 0; + break; + } + } + + return i; +} + +static size_t playback(struct buffer *b, float *out, size_t n) +{ + size_t i = 0; + + for (; i < n; i++) + { + float t = b->phase*b->len; + size_t i0 = (size_t) t; + size_t i1 = i0 + 1; + float ip = t - i0; + + i0 %= b->len; + i1 %= b->len; + + float s0 = b->data[i0]; + float s1 = b->data[i1]; + + out[i] = s0 + ip*(s1 - s0); + + b->phase += b->speed/b->len; + + if (b->phase >= 1) + { + b->phase = 0; + b->mode = IDLE; + } + } + + return i; +} + +static size_t idle(struct buffer *b, float *out, size_t n) +{ + memset(out,0,sizeof(*out)*n); + return n; +} + +static void proc(const ml_proc_args *args) +{ + struct buffer *b = args->userp; + const float *in = args->in[0]; + float *out = args->out[0]; + size_t n = args->nframes; + + while (n > 0) + { + size_t d; + + switch (b->mode) + { + case CAPTURE: + d = capture(b,in,n); + break; + case PLAYBACK: + d = playback(b,out,n); + break; + case IDLE: + d = idle(b,out,n); + break; + } + + in += d; + out += d; + n -= d; + } +} + +static int l_init(lua_State *s) +{ + float t = luaL_checknumber(s,1); + ml_channel *src = mutil_get_channel(s,2); + + if (t <= 0) + { + luaL_error(s,"invalid buffer capacity %.2f\n",t); + } + + size_t sr = ml_sample_rate(); + size_t cap = t*sr; + + struct buffer *b; + size_t size = sizeof(*b) + sizeof(*b->data)*cap; + b = calloc(1,size); + + ml_node *node = ml_create_node(proc,b); + ml_label_node(node,"buffer"); + + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + + b->mode = IDLE; + b->len = 0; + b->phase = 0; + b->cap = cap; + b->chan = ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = b; + luaL_setmetatable(s,BUFFER_TYPENAME); + + return 1; +} + +static int l_setmode(lua_State *s) +{ + int mode_i = lua_upvalueindex(1); + struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME); + enum mode mode = lua_tointeger(s,mode_i); + b->phase = 0; + b->mode = mode; + + if (mode == CAPTURE) + { + b->len = 0; + } + + return 0; +} + +static int l_setspeed(lua_State *s) +{ + struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME); + double speed = luaL_checknumber(s,2); + + if (speed <= 0) + { + luaL_error(s,"invalid speed %.2f",speed); + } + + b->speed = speed; + return 0; +} + +static int l_getlength(lua_State *s) +{ + struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME); + size_t sr = ml_sample_rate(); + lua_pushnumber(s,(float)b->len / (float)sr / b->speed); + return 1; +} + +static int l_out(lua_State *s) +{ + struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = b->chan; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_buffer(lua_State *s) +{ +/*** Audio buffer for capture/playback. + + local mbuffer = require 'melisma.buffer' + local mevent = require 'melisma.event' + + local input = ... + + -- Create a buffer with capacity of 5 seconds + -- Capture stops automatically when buffer is full. + local len = 5 + local buffer = mbuffer.init(input, len) + + -- Capture audio + buffer:capture() + + -- Playback after capture 3 seconds + mevent.schedule(3, buffer.playback, buffer) + +@classmod buffer +*/ + lua_newtable(s); + int tbl = lua_absindex(s,lua_gettop(s)); + +/*** Create an audio buffer. +@static +@function buffer.init +@tparam channel input Input signal. +@tparam number len Buffer capacity in seconds. +@treturn buffer The newly created audio buffer. +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Set playback speed. +@function buffer.speed +@tparam number speed Playback speed. +*/ + lua_pushcfunction(s,l_setspeed); + lua_setfield(s,tbl,"speed"); + +/*** Get length of captured audio. +@function buffer.length +@treturn number Length of captured audio in seconds. +*/ + lua_pushcfunction(s,l_getlength); + lua_setfield(s,tbl,"length"); + +/*** Start playback mode. +@function buffer.playback +*/ + lua_pushinteger(s,PLAYBACK); + lua_pushcclosure(s,l_setmode,1); + lua_setfield(s,tbl,"playback"); + +/*** Start capture mode. +@function buffer.capture +*/ + lua_pushinteger(s,CAPTURE); + lua_pushcclosure(s,l_setmode,1); + lua_setfield(s,tbl,"capture"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,BUFFER_TYPENAME); + int mt = lua_absindex(s,lua_gettop(s)); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/delay.c b/mod/delay.c new file mode 100644 index 0000000..44441cf --- /dev/null +++ b/mod/delay.c @@ -0,0 +1,233 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define DELAY_TYPENAME "delay" + +#define MAX_DELAY 2.00f +#define DEFAULT_DELAY 0.25f +#define DEFAULT_GAIN 1.00f +#define DEFAULT_FEEDBACK 0.25f +#define DEFAULT_WETDRYMIX 0.50f + +#define CLAMP(v,min,max) ((v)<(min)?(min):((v)>(max)?(max):(v))) + +struct delay +{ + ml_channel *out; + float feedback; + float gain; + float wetdrymix; + float delay; + size_t len; + size_t write; + float *buffer; +}; + +static void proc(const ml_proc_args *args) +{ + struct delay *dl = args->userp; + + size_t sr = ml_sample_rate(); + size_t offset = sr*dl->delay; + + const float *in = args->in[0]; + float *out = args->out[0]; + + size_t read; + float dry,wet; + + for (size_t i = 0; i < args->nframes; i++) + { + read = dl->write + dl->len - offset; + + if (read >= dl->len) + { + read -= dl->len; + } + + dry = in[i]; + wet = dl->buffer[read]*dl->gain; + + out[i] = wet*dl->wetdrymix + dry*(1 - dl->wetdrymix); + dl->buffer[dl->write++] = dry + wet*dl->feedback; + + if (dl->write == dl->len) + { + dl->write = 0; + } + } +} + +static void clamp(struct delay *dl) +{ + dl->delay = CLAMP(dl->delay,0,MAX_DELAY); + dl->gain = CLAMP(dl->gain,0,1); + dl->feedback = CLAMP(dl->feedback,0,1); + dl->wetdrymix = CLAMP(dl->wetdrymix,0,1); +} + +static int l_init(lua_State *s) +{ + ml_channel *src = mutil_get_channel(s,1); + + struct delay *dl = calloc(1,sizeof(*dl)); + + dl->delay = DEFAULT_DELAY; + dl->gain = DEFAULT_GAIN; + dl->feedback = DEFAULT_FEEDBACK; + dl->wetdrymix = DEFAULT_WETDRYMIX; + + size_t sr = ml_sample_rate(); + dl->len = sr*MAX_DELAY; + dl->buffer = calloc(1,dl->len*sizeof(dl->buffer[0])); + + clamp(dl); + + ml_node *node = ml_create_node(proc,dl); + ml_label_node(node,"delay"); + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + + dl->out = ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = dl; + luaL_setmetatable(s,DELAY_TYPENAME); + + return 1; +} + +static int l_out(lua_State *s) +{ + struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = dl->out; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +static int l_delay(lua_State *s) +{ + struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME); + float v = luaL_checknumber(s,2); + dl->delay = v; + clamp(dl); + *(void**) lua_newuserdata(s,sizeof(void*)) = dl; + luaL_setmetatable(s,DELAY_TYPENAME); + return 1; +} + +static int l_gain(lua_State *s) +{ + struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME); + float v = luaL_checknumber(s,2); + dl->gain = v; + clamp(dl); + *(void**) lua_newuserdata(s,sizeof(void*)) = dl; + luaL_setmetatable(s,DELAY_TYPENAME); + return 1; +} + +static int l_feedback(lua_State *s) +{ + struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME); + float v = luaL_checknumber(s,2); + dl->feedback = v; + clamp(dl); + *(void**) lua_newuserdata(s,sizeof(void*)) = dl; + luaL_setmetatable(s,DELAY_TYPENAME); + return 1; +} + +static int l_wetdrymix(lua_State *s) +{ + struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME); + float v = luaL_checknumber(s,2); + dl->wetdrymix = v; + clamp(dl); + *(void**) lua_newuserdata(s,sizeof(void*)) = dl; + luaL_setmetatable(s,DELAY_TYPENAME); + return 1; +} + +int luaopen_melisma_delay(lua_State *s) +{ + ml_check_version(); + +/*** Digital delay line. +@classmod delay +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Create a delay line. +@static +@function init +@tparam channel input Input signal. +@treturn delay Newly created delay line. +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Set delay. +@function delay +@tparam number d delay in seconds. +@treturn delay +*/ + lua_pushcfunction(s,l_delay); + lua_setfield(s,tbl,"delay"); + +/*** Set gain. +@function gain +@tparam number g Gain (**TODO**: Use decibels). +@treturn delay +*/ + lua_pushcfunction(s,l_gain); + lua_setfield(s,tbl,"gain"); + +/*** Set feedback. +@function feedback +@tparam number f Feedback. +@treturn delay +*/ + lua_pushcfunction(s,l_feedback); + lua_setfield(s,tbl,"feedback"); + +/*** Set wet/dry mix. +@function wetdrymix +@tparam number wd Wet/dry mix. +@treturn delay +*/ + lua_pushcfunction(s,l_wetdrymix); + lua_setfield(s,tbl,"wetdrymix"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,DELAY_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} 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 . + */ + +#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; +} diff --git a/mod/fs.c b/mod/fs.c new file mode 100644 index 0000000..e08dfda --- /dev/null +++ b/mod/fs.c @@ -0,0 +1,103 @@ +/* + * 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 + +static const char *dirent_typestr(struct dirent *de) +{ + switch (de->d_type) + { + case DT_BLK: return "b"; + case DT_CHR: return "c"; + case DT_DIR: return "d"; + case DT_FIFO: return "p"; + case DT_REG: return "f"; + case DT_LNK: return "l"; + case DT_SOCK: return "s"; + default: return "?"; + } +} + +static void push_dirent(lua_State *s, struct dirent *de) +{ + lua_newtable(s); + int tbl = lua_gettop(s); + + lua_pushstring(s,de->d_name); + lua_setfield(s,tbl,"name"); + + lua_pushstring(s,dirent_typestr(de)); + lua_setfield(s,tbl,"type"); + + lua_settop(s,tbl); +} + +static int l_readdir(lua_State *s) +{ + const char *path = luaL_checkstring(s,1); + DIR *dirp = opendir(path); + + if (dirp == NULL) + { + luaL_error(s,strerror(errno)); + } + + lua_newtable(s); + int tbl = lua_gettop(s); + + int i = 1; + struct dirent *de; + while ((de = readdir(dirp)) != NULL) + { + push_dirent(s,de); + lua_rawseti(s,tbl,i++); + } + + closedir(dirp); + lua_settop(s,tbl); + return 1; +} + +int luaopen_melisma_fs(lua_State *s) +{ +/*** File system utilities. +@module fs +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** List directory contents. +@function readdir +@tparam string path Directory path. +@treturn table Directory contents. +@usage +local fs = require 'melisma.fs' +local contents = fs.readdir('.') +for _,entry in ipairs(contents) do + print(entry.name, entry.type) +end +*/ + lua_pushcfunction(s,l_readdir); + lua_setfield(s,tbl,"readdir"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/gain.c b/mod/gain.c new file mode 100644 index 0000000..994efcf --- /dev/null +++ b/mod/gain.c @@ -0,0 +1,164 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define GAIN_TYPENAME "gain" + +struct gain +{ + ml_node *node; + float value; + int mapped; + int db; +}; + +static void proc(const ml_proc_args *args) +{ + struct gain *gain = args->userp; + + float *in = args->in[0]; + float *out = args->out[0]; + float *mapped = gain->mapped ? args->in[1] : NULL; + float v; + + for (size_t i = 0; i < args->nframes; i++) + { + v = mapped ? mapped[i] : gain->value; + + if (gain->db) + { + v = powf(10,v/20.f); + } + + out[i] = v*in[i]; + } +} + +static int l_init(lua_State *s) +{ + int db_i = lua_upvalueindex(1); + int db = lua_tointeger(s,db_i); + ml_channel *audio_src, *mapped_src = NULL; + ml_channel *audio_dst, *mapped_dst; + float value = 0; + + + if (lua_isuserdata(s,1)) + { + mapped_src = mutil_get_channel(s,1); + } + else + { + value = luaL_checknumber(s,1); + } + + audio_src = mutil_get_channel(s,2); + + struct gain *gain = calloc(1,sizeof(*gain)); + gain->value = value; + gain->mapped = mapped_src != NULL; + gain->db = db; + + ml_node *node = ml_create_node(proc,gain); + ml_label_node(node,"gain"); + gain->node = node; + + ml_add_output(node); + + audio_dst = ml_add_input(node); + ml_connect(audio_src,audio_dst); + + if (mapped_src) + { + mapped_dst = ml_add_input(node); + ml_connect(mapped_src,mapped_dst); + } + + *(void**) lua_newuserdata(s,sizeof(void*)) = gain; + luaL_setmetatable(s,GAIN_TYPENAME); + + return 1; +} + +static int l_out(lua_State *s) +{ + struct gain *gain = *(void**) luaL_checkudata(s,1,GAIN_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(gain->node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_gain(lua_State *s) +{ + ml_check_version(); + +/*** Apply gain to signals. + local mgain = require 'melisma.gain' + + -- Interprets `cntl` as decibels and applies it to `input` + local input = ... + local cntl = ... + local gain = mgain.db(input,cntl) + +@classmod gain +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Creates a linear gain unit. +@static +@function linear +@tparam channel input Input signal. +@tparam number|channel gain Constant gain or a controlling signal. +@treturn gain The newly created gain unit. +*/ + lua_pushinteger(s,0); + lua_pushcclosure(s,l_init,1); + lua_setfield(s,tbl,"linear"); + +/*** Creates a decibel gain unit. +@static +@function db +@tparam channel input Input signal. +@tparam number|channel gain Constant gain in decibels or a controlling signal. +@treturn gain The newly created gain unit. +*/ + lua_pushinteger(s,1); + lua_pushcclosure(s,l_init,1); + lua_setfield(s,tbl,"db"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,GAIN_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/interp.c b/mod/interp.c new file mode 100644 index 0000000..c5bbefb --- /dev/null +++ b/mod/interp.c @@ -0,0 +1,257 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define INTERP_TYPENAME "interp" +#define DEFAULT_METHOD "linear" + +struct interp; +typedef float (*smp_fn)(float); + +enum { + LINEAR, + EASEIN, + EASEOUT, + SMOOTHSTEP, +}; + +struct interp +{ + ml_channel *out; + smp_fn smp; + float src; + float dst; + size_t t0; + size_t t; + int method; +}; + +static float sample(struct interp *ip) +{ + float x = ip->t0 == 0 ? 0 : (float)ip->t / (float)ip->t0; + switch (ip->method) + { + case LINEAR: + break; + case EASEIN: + x = x*x; + break; + case EASEOUT: + x = 1 -x; + x = 1 - x*x; + break; + case SMOOTHSTEP: + x = x*x*(3 - 2*x); + break; + } + return ip->src + x*(ip->dst - ip->src); +} + +static void proc(const ml_proc_args *args) +{ + struct interp *ip = args->userp; + + float *out = args->out[0]; + + for (size_t i = 0; i < args->nframes; i++) + { + out[i] = sample(ip); + + if (ip->t == ip->t0) + { + ip->t = 0; + ip->t0 = 0; + ip->src = ip->dst; + } + else + { + ip->t++; + } + } +} + +static int l_init(lua_State *s) +{ + float v = luaL_checknumber(s,1); + + struct interp *ip = calloc(1,sizeof(*ip)); + ip->src = v; + ip->dst = v; + ip->method = LINEAR; + + ml_node *node = ml_create_node(proc,ip); + ml_label_node(node,"interp"); + ip->out = ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = ip; + luaL_setmetatable(s,INTERP_TYPENAME); + + return 1; +} + +static int l_set(lua_State *s) +{ + struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME); + float v = luaL_checknumber(s,2); + + ip->src = v; + ip->dst = v; + ip->t0 = 0; + ip->t = 0; + + return 0; +} + +static int l_target(lua_State *s) +{ + int method_i = lua_upvalueindex(1); + int method = luaL_checkinteger(s,method_i); + struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME); + float v = luaL_checknumber(s,2); + float t = luaL_checknumber(s,3); + + size_t sr = ml_sample_rate(); + + ip->src = sample(ip); + ip->dst = v; + ip->t0 = sr*t; + ip->t = 0; + ip->method = method; + + return 0; +} + +static int l_sample(lua_State *s) +{ + struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME); + lua_pushnumber(s,sample(ip)); + return 1; +} + +static int l_out(lua_State *s) +{ + struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ip->out; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_interp(lua_State *s) +{ + ml_check_version(); + +/*** Interpolation. + + local minterp = require 'melisma.interp' + local mevent = require 'melisma.event' + local mgain = require 'melisma.gain' + + local interp = minterp.init(0) + + interp:linear(1,1) + + mevent.schedule(0.5, function() + print('current value:', interp:sample()) + end) + + local sig = ... + local gain = mgain.linear(sig,interp) + +@classmod interp +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Initialize an interpolation unit. +@static +@function init +@tparam number x0 Initial value. +@treturn interp The newly created interpolation unit. +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Set value directly. +@function set +@tparam number x New value. +*/ + lua_pushcfunction(s,l_set); + lua_setfield(s,tbl,"set"); + +/*** Interpolate using linear interpolation. +@function linear +@tparam number v Target value. +@tparam number t Interpolation time. +*/ + lua_pushnumber(s,LINEAR); + lua_pushcclosure(s,l_target,1); + lua_setfield(s,tbl,"linear"); + + +/*** Interpolate using ease-in interpolation. +@function ease_in +@tparam number v Target value. +@tparam number t Interpolation time. +*/ + lua_pushnumber(s,EASEIN); + lua_pushcclosure(s,l_target,1); + lua_setfield(s,tbl,"ease_in"); + + +/*** Interpolate using ease-out interpolation. +@function ease_out +@tparam number v Target value. +@tparam number t Interpolation time. +*/ + lua_pushnumber(s,EASEOUT); + lua_pushcclosure(s,l_target,1); + lua_setfield(s,tbl,"ease_out"); + +/*** Interpolate using smoothstep interpolation. +@function smoothstep +@tparam number v Target value. +@tparam number t Interpolation time. +*/ + lua_pushnumber(s,SMOOTHSTEP); + lua_pushcclosure(s,l_target,1); + lua_setfield(s,tbl,"smoothstep"); + +/*** Get the current value. +@function sample +@treturn number The current value. +*/ + lua_pushcfunction(s,l_sample); + lua_setfield(s,tbl,"sample"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,INTERP_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/keycallback.c b/mod/keycallback.c new file mode 100644 index 0000000..2352cbf --- /dev/null +++ b/mod/keycallback.c @@ -0,0 +1,147 @@ +/* + * 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 "melisma/ring.h" + +static int fn = LUA_NOREF; +static struct termios tty = {0}; +static struct termios oldtty = {0}; +static lua_State *state; +static ml_ring *ring = NULL; + +static void make_arg(lua_State *s, size_t len, const void *data) +{ + lua_pushlstring(s,data,len); +} + +static void io_handler(int fd, int flags, void *args) +{ + char c; + + if (read(fd,&c,sizeof c) <= 0) + { + return; + } + + ml_ring_put_blocking(ring,*(uint8_t*) &c); +} + +static void cleanup(void) +{ + tcsetattr(STDIN_FILENO,TCSANOW,&oldtty); +} + +static void proc(const ml_proc_args *args) +{ + if (fn == LUA_NOREF) + { + return; + } + + char buf[1]; + while (ml_ring_read(ring,buf,sizeof buf) == sizeof buf) + { + lua_pushlstring(state,buf,sizeof buf); + int arg = luaL_ref(state,LUA_REGISTRYINDEX); + ml_schedule_event(0,fn,arg); + } +} + +static void setup(lua_State *s) +{ + static bool init = false; + + if (init) + { + return; + } + + init = true; + + tcgetattr(STDIN_FILENO,&oldtty); + tty = oldtty; + tty.c_lflag &= ~ECHO; + tty.c_lflag &= ~ICANON; + tcsetattr(STDIN_FILENO,TCSANOW,&tty); + tcflush(STDIN_FILENO,TCIFLUSH); + + ml_add_io_handler(STDIN_FILENO,POLLIN,io_handler,NULL); + ring = ml_ring_init(4096); + state = s; + + ml_node *node = ml_create_node(proc,NULL); + ml_channel *src = ml_add_output(node); + ml_channel *dst = ml_add_input(ml_get_root_node()); + ml_connect(src,dst); + ml_label_node(node,"keycallback"); + + atexit(cleanup); +} + +static int l_set(lua_State *s) +{ + if (fn != LUA_NOREF) + { + luaL_unref(s,LUA_REGISTRYINDEX,fn); + fn = LUA_NOREF; + } + + if (lua_isnil(s,1)) + { + return 0; + } + + luaL_checktype(s,1,LUA_TFUNCTION); + lua_pushvalue(s,1); + fn = luaL_ref(s,LUA_REGISTRYINDEX); + + setup(s); + + return 0; +} + +int luaopen_melisma_keycallback(lua_State *s) +{ +/*** Set callback for keyboard input. + local mkeycallback = require 'melisma.keycallback' + + mkeycallback.set(function(c) + print('you pressed', c) + end) +@module keycallback +*/ + lua_newtable(s); + int tbl = lua_absindex(s,lua_gettop(s)); + +/*** Set the callback. +@function set +@tparam function fn Callback function. +*/ + lua_pushcfunction(s,l_set); + lua_setfield(s,tbl,"set"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/ladspa.c b/mod/ladspa.c new file mode 100644 index 0000000..4b681b9 --- /dev/null +++ b/mod/ladspa.c @@ -0,0 +1,604 @@ +/* + * 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; +} diff --git a/mod/lookback.c b/mod/lookback.c new file mode 100644 index 0000000..115f823 --- /dev/null +++ b/mod/lookback.c @@ -0,0 +1,106 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define LOOKBACK_TYPENAME "lookback" + +struct lookback +{ + ml_node *node; + size_t cursor; + size_t len; + float buf[]; +}; + +static void proc(const ml_proc_args *args) +{ + struct lookback *lb = args->userp; + + for (size_t i = 0; i < args->nframes; i++) + { + args->out[0][i] = lb->buf[lb->cursor]; + lb->buf[lb->cursor] = args->in[0][i]; + + lb->cursor++; + + if (lb->cursor == lb->len) + { + lb->cursor = 0; + } + } +} + +static int l_init(lua_State *s) +{ + float t = luaL_checknumber(s,1); + ml_channel *src = mutil_get_channel(s,2); + + unsigned long sr = ml_sample_rate(); + unsigned long len = sr*t; + + struct lookback *lb = calloc(1,sizeof(*lb) + len*sizeof(lb->buf[0])); + lb->len = len; + + ml_node *node = ml_create_node(proc,lb); + ml_label_node(node,"lookback"); + lb->node = node; + + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = lb; + luaL_setmetatable(s,LOOKBACK_TYPENAME); + + return 1; +} + +static int l_out(lua_State *s) +{ + struct lookback *lb = *(void**) luaL_checkudata(s,1,LOOKBACK_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(lb->node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_lookback(lua_State *s) +{ + lua_newtable(s); + int tbl = lua_gettop(s); + + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,LOOKBACK_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/map.h b/mod/map.h new file mode 100644 index 0000000..c56487e --- /dev/null +++ b/mod/map.h @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +#ifndef MAP_H +#define MAP_H + +#include "melisma.h" +#include + +struct map +{ + float value; + int channel; +}; + +static void map_from_arg(lua_State *s, struct map *m, int arg) +{ + if (lua_isnumber(s,arg)) + { + } + + if (lua_isuserdata(s,arg)) + { + } + + luaL_argerror(s,arg,"expected number or output channel"); +} + +static float map_get(struct map *m, size_t i, const ml_proc_args *args) +{ + return m->channel >= 0 ? args->in[m->channel][i] : m->value; +} + +#endif diff --git a/mod/mixer.c b/mod/mixer.c new file mode 100644 index 0000000..ab5ade2 --- /dev/null +++ b/mod/mixer.c @@ -0,0 +1,114 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define MIXER_TYPENAME "mixer" + +static void proc(const ml_proc_args *args) +{ + float *out = args->out[0]; + + memset(out,0,args->nframes * sizeof(out[0])); + + for (size_t i = 0; i < args->icnt; i++) + { + const float *in = args->in[i]; + + for (size_t j = 0; j < args->nframes; j++) + { + out[j] += in[j]; + } + } +} + +static int l_init(lua_State *s) +{ + int n = lua_gettop(s); + + if (n == 0) + { + luaL_error(s,"need at least one input channel"); + } + + ml_node *node = ml_create_node(proc,NULL); + ml_label_node(node,"mix"); + + for (int i = 0; i < n; i++) + { + ml_channel *src = mutil_get_channel(s,i+1); + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + } + + ml_channel *out = ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = out; + luaL_setmetatable(s,MIXER_TYPENAME); + + return 1; +} + +static int l_out(lua_State *s) +{ + ml_channel *c = *(void**) luaL_checkudata(s,1,MIXER_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = c; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_mixer(lua_State *s) +{ + ml_check_version(); + +/*** Mix signals. + + local mmixer = require 'melisma.mixer' + + local signals = ... + local mixer = mmixer.init(table.unpack(signals)) + +@classmod mixer +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Mix input signals in to a single output signal. +@static +@function init +@tparam channel ... Input signals. +@treturn mixer The newly created mixer unit. +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,MIXER_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/mutil.h b/mod/mutil.h new file mode 100644 index 0000000..215508b --- /dev/null +++ b/mod/mutil.h @@ -0,0 +1,69 @@ +/* + * 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 . + */ + +#ifndef MUTIL_H +#define MUTIL_H + +/* TODO: Move to main repo? */ + +#include "melisma.h" +#include + +#define MUTIL_OUTPUT_CHANNEL_FUNCTION "__ml_out" + +static ml_channel *mutil_get_optchannel(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,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + if (!lua_isfunction(s,-1)) + { + lua_pop(s,1); + return NULL; + } + + 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); + return NULL; +} + +static ml_channel *mutil_get_channel(lua_State *s, int i) +{ + ml_channel *c = mutil_get_optchannel(s,i); + if (c == NULL) + { + luaL_argerror(s,i,"expected output channel"); + } + return c; +} + +#endif diff --git a/mod/null.c b/mod/null.c new file mode 100644 index 0000000..815d622 --- /dev/null +++ b/mod/null.c @@ -0,0 +1,76 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define NULL_TYPENAME "null" + +static void proc(const ml_proc_args *args) +{ + assert(args->ocnt == 1); + memset(args->out[0],0,sizeof(*args->out[0])*args->nframes); +} + +static int l_init(lua_State *s) +{ + ml_node *node = ml_create_node(proc,NULL); + ml_label_node(node,"null"); + + (void) ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = node; + luaL_setmetatable(s,NULL_TYPENAME); + + return 1; +} + +static int l_out(lua_State *s) +{ + ml_node *node = *(void**) luaL_checkudata(s,1,NULL_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_null(lua_State *s) +{ + ml_check_version(); + + lua_newtable(s); + int tbl = lua_gettop(s); + + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,NULL_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/nullsink.c b/mod/nullsink.c new file mode 100644 index 0000000..0cbd810 --- /dev/null +++ b/mod/nullsink.c @@ -0,0 +1,111 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define NULLSINK_TYPENAME "nullsink" + +static int l_init(lua_State *s) +{ + ml_node *node = ml_create_node(NULL,NULL); + ml_label_node(node,"nullsink"); + ml_node *root = ml_get_root_node(); + + ml_channel *src = ml_add_output(node); + ml_channel *dst = ml_add_input(root); + + ml_connect(src,dst); + + *(void**) lua_newuserdata(s,sizeof(void*)) = node; + luaL_setmetatable(s,NULLSINK_TYPENAME); + + return 1; +} + +static int l_ref(lua_State *s) +{ + ml_node *node = *(void**) luaL_checkudata(s,1,NULLSINK_TYPENAME); + ml_channel *src = mutil_get_channel(s,2); + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + *(void**) lua_newuserdata(s,sizeof(void*)) = node; + luaL_setmetatable(s,NULLSINK_TYPENAME); + return 1; +} + +int luaopen_melisma_nullsink(lua_State *s) +{ + ml_check_version(); + +/*** Reference otherwise disconnected nodes. + + local mnullsink = require 'melisma.nullsink' + local minterp = require 'melisma.interp' + local mevent = require 'melisma.event' + + local interp = minterp.init(0) + interp:linear(1,60) + + -- The output of interp is disconnected. + -- Need to connect it to a nullsink so we + -- can sample it in the event callback below. + local nullsink = mnullsink.init() + nullsink:ref(interp) + + mevent.schedule(30, function() + print('current value:', interp:sample()) + end) + +@classmod nullsink +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Create a nullsink. +@static +@function init +@treturn nullsink The newly created nullsink. +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Connect an input signal. + +This makes sure the input signal is processed +even if it's not connected to anything else. + +@function ref +@tparam channel input Input signal. +@treturn nullsink +*/ + lua_pushcfunction(s,l_ref); + lua_setfield(s,tbl,"ref"); + + luaL_newmetatable(s,NULLSINK_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/oscil.c b/mod/oscil.c new file mode 100644 index 0000000..7bc39c7 --- /dev/null +++ b/mod/oscil.c @@ -0,0 +1,438 @@ +/* + * 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 WAVETABLE_TYPENAME "wavetable" +#define OSCIL_TYPENAME "oscil" + +#ifndef WAVETABLE_SIZE +#define WAVETABLE_SIZE 2048 +#endif + +#define WAVETABLE_ALLOC_SIZE (sizeof(float)*WAVETABLE_SIZE) + +struct oscil +{ + ml_node *node; + unsigned long clk; + float f; + float a; + int mf; + int ma; + float p; + float v; + float tbl[WAVETABLE_SIZE]; +}; + +static float *push_new_wavetable(lua_State *s) +{ + float *ptr = lua_newuserdata(s,WAVETABLE_ALLOC_SIZE); + luaL_setmetatable(s,WAVETABLE_TYPENAME); + memset(ptr,0,WAVETABLE_ALLOC_SIZE); + return ptr; +} + +static int l_const(lua_State *s) +{ + float v = luaL_checknumber(s,1); + float *tbl = push_new_wavetable(s); + + for (size_t i = 0; i < WAVETABLE_SIZE; i++) + { + tbl[i] = v; + } + + return 1; +} + +static int l_sin(lua_State *s) +{ + float *tbl = push_new_wavetable(s); + + for (size_t i = 0; i < WAVETABLE_SIZE; i++) + { + float f = 2.f * M_PI * (float) i / (float) WAVETABLE_SIZE; + tbl[i] = sinf(f); + } + + return 1; +} + +static int l_saw(lua_State *s) +{ + float *tbl = push_new_wavetable(s); + + for (size_t i = 0; i < WAVETABLE_SIZE; i++) + { + tbl[i] = 2 * (float) i / (float) WAVETABLE_SIZE - 1; + } + + return 1; +} + +static int l_square(lua_State *s) +{ + float *tbl = push_new_wavetable(s); + + for (size_t i = 0; i < WAVETABLE_SIZE; i++) + { + if (i >= WAVETABLE_SIZE/2) + { + tbl[i] = 1; + } + else + { + tbl[i] = -1; + } + } + + return 1; +} + +static int l_shift(lua_State *s) +{ + float *src = luaL_checkudata(s,1,WAVETABLE_TYPENAME); + float p = luaL_checknumber(s,2); + + while (p < 0) + { + p += 2*M_PI; + } + + while (p > 2*M_PI) + { + p -= 2*M_PI; + } + + size_t offset = p/2.f/M_PI*WAVETABLE_SIZE; + float *dst = push_new_wavetable(s); + + size_t i,d; + for (i=0, d=offset; i < WAVETABLE_SIZE; i++, d++) + { + dst[d % WAVETABLE_SIZE] = src[i]; + } + + return 1; +} + +static int l_scale(lua_State *s) +{ + float *src = luaL_checkudata(s,1,WAVETABLE_TYPENAME); + int n = luaL_checkinteger(s,2); + + // if (n <= 0 || (n & (n-1)) != 0) + // { + // luaL_error(s,"scaling factor must be a power of two"); + // } + + float *dst = push_new_wavetable(s); + + for (size_t i = 0; i < WAVETABLE_SIZE; i++) + { + size_t j = (n*i) % WAVETABLE_SIZE; + dst[i] = src[j]; + } + + return 1; +} + +static int l_add(lua_State *s) +{ + float *lhs = luaL_checkudata(s,1,WAVETABLE_TYPENAME); + float *rhs = luaL_checkudata(s,2,WAVETABLE_TYPENAME); + float *res = push_new_wavetable(s); + + for (size_t i = 0; i < WAVETABLE_SIZE; i++) + { + res[i] = lhs[i] + rhs[i]; + } + + return 1; +} + +static void proc(const ml_proc_args *args) +{ + struct oscil *oscil = args->userp; + + unsigned long sr = ml_sample_rate(); + float p = oscil->p; + float *dst = args->out[0]; + + float *mf = oscil->mf < 0 ? NULL : args->in[oscil->mf]; + float *ma = oscil->ma < 0 ? NULL : args->in[oscil->ma]; + + for (size_t i = 0; i < args->nframes; i++) + { + float f = mf ? mf[i] : oscil->f; + float a = ma ? ma[i] : oscil->a; + + float t = p*WAVETABLE_SIZE; + + size_t i0 = (size_t) t; + size_t i1 = i0 + 1; + + float ip = t - i0; + + i0 &= (WAVETABLE_SIZE-1); + i1 &= (WAVETABLE_SIZE-1); + + float s0 = oscil->tbl[i0]; + float s1 = oscil->tbl[i1]; + + dst[i] = a*(s0 + ip*(s1 - s0)); + + p += f/sr; + + if (p > 1) + { + p -= 1; + } + } + + oscil->p = p; + oscil->v = dst[args->nframes-1]; +} + +static int l_init(lua_State *s) +{ + float *tbl = luaL_checkudata(s,1,WAVETABLE_TYPENAME); + float f = luaL_checknumber(s,2); + float a = luaL_checknumber(s,3); + + struct oscil *oscil = calloc(1,sizeof(*oscil)); + oscil->clk = 0; + oscil->f = f; + oscil->a = a; + oscil->mf = -1; + oscil->ma = -1; + oscil->p = 0; + oscil->v = a*tbl[0]; + + ml_node *node = ml_create_node(proc,oscil); + ml_label_node(node,"oscil"); + ml_add_output(node); + oscil->node = node; + + memcpy(oscil->tbl,tbl,WAVETABLE_ALLOC_SIZE); + + *(void**) lua_newuserdata(s,sizeof(void*)) = oscil; + luaL_setmetatable(s,OSCIL_TYPENAME); + + return 1; +} + +static int l_setf(lua_State *s) +{ + struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME); + float v = luaL_checknumber(s,2); + oscil->f = v; + return 0; +} + +static int l_seta(lua_State *s) +{ + struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME); + float v = luaL_checknumber(s,2); + oscil->a = v; + return 0; +} + +static int l_mapf(lua_State *s) +{ + struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME); + ml_channel *src = mutil_get_channel(s,2); + + oscil->mf = ml_num_inputs(oscil->node); + + ml_channel *dst = ml_add_input(oscil->node); + ml_connect(src,dst); + + return 0; +} + +static int l_mapa(lua_State *s) +{ + struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME); + ml_channel *src = mutil_get_channel(s,2); + + oscil->ma = ml_num_inputs(oscil->node); + + ml_channel *dst = ml_add_input(oscil->node); + ml_connect(src,dst); + + return 0; +} + +static int l_sample(lua_State *s) +{ + struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME); + lua_pushnumber(s,oscil->v); + return 1; +} + +static int l_out(lua_State *s) +{ + struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(oscil->node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_oscil(lua_State *s) +{ + +/*** Wavetable backed oscillators. + + local moscil = require 'melisma.oscil' + local mexpression = require 'melisma.expression' + + local wt = moscil.wavetable.sin() + + -- Make an LFO for controlling the frequency. + local lfo_oscil = moscil.init(wt,1,1) + + -- Map [-1, 1] to [440, 880] + local lfo = (440 + (1 + mexpression.input(lfo_oscil,0) / 2) * 440):build() + + local oscil = moscil.init(wt,0,0.5) + oscil:mapf(lfo) + +@classmod oscil +*/ + lua_newtable(s); + int tbl = lua_absindex(s,lua_gettop(s)); + +/*** Initialize an oscillator. +@static +@function init +@tparam wavetable wt +@tparam number freq +@tparam number amp +@treturn oscil +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Set oscillator frequency. +@function setf +@tparam number freq +*/ + lua_pushcfunction(s,l_setf); + lua_setfield(s,tbl,"setf"); + +/*** Set oscillator amplitude. +@function seta +@tparam number amp +*/ + lua_pushcfunction(s,l_seta); + lua_setfield(s,tbl,"seta"); + +/*** Map a signal to the frequency. +@function mapf +@tparam channel sig +*/ + lua_pushcfunction(s,l_mapf); + lua_setfield(s,tbl,"mapf"); + +/*** Map a signal to the amplitude. +@function mapa +@tparam channel sig +*/ + lua_pushcfunction(s,l_mapa); + lua_setfield(s,tbl,"mapa"); + +/*** Get the current oscillator value. +@function sample +@treturn number +*/ + lua_pushcfunction(s,l_sample); + lua_setfield(s,tbl,"sample"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,OSCIL_TYPENAME); + int oscil_mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,oscil_mt,"__index"); + +/*** +@type wavetable +*/ + lua_newtable(s); + int wt_tbl = lua_absindex(s,lua_gettop(s)); + + lua_pushvalue(s,wt_tbl); + lua_setfield(s,tbl,"wavetable"); + + lua_pushcfunction(s,l_const); + lua_setfield(s,wt_tbl,"const"); + +/*** Sine wave. +@static +@function wavetable.sin +@treturn wavetable +*/ + lua_pushcfunction(s,l_sin); + lua_setfield(s,wt_tbl,"sin"); + +/*** Saw wave. +@static +@function wavetable.saw +@treturn wavetable +*/ + lua_pushcfunction(s,l_saw); + lua_setfield(s,wt_tbl,"saw"); + +/*** Square wave. +@static +@function wavetable.square +@treturn wavetable +*/ + lua_pushcfunction(s,l_square); + lua_setfield(s,wt_tbl,"square"); + + lua_pushcfunction(s,l_shift); + lua_setfield(s,wt_tbl,"shift"); + + lua_pushcfunction(s,l_scale); + lua_setfield(s,wt_tbl,"scale"); + + luaL_newmetatable(s,WAVETABLE_TYPENAME); + int wt_mt = lua_gettop(s); + + lua_pushvalue(s,wt_tbl); + lua_setfield(s,wt_mt,"__index"); + + lua_pushcfunction(s,l_add); + lua_setfield(s,wt_mt,"__add"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/pan.c b/mod/pan.c new file mode 100644 index 0000000..84a0551 --- /dev/null +++ b/mod/pan.c @@ -0,0 +1,187 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define PAN_TYPENAME "pan" + +struct pan +{ + ml_channel *l_out; + ml_channel *r_out; + float val; + bool map; +}; + +static void proc(const ml_proc_args *args) +{ + struct pan *pan = args->userp; + + float val,*in,*l_out,*r_out,*map; + in = args->in[0]; + l_out = args->out[0]; + r_out = args->out[1]; + map = pan->map ? args->in[1] : NULL; + + for (size_t i = 0; i < args->nframes; i++) + { + val = map ? map[i] : pan->val; + l_out[i] = val*in[i]; + r_out[i] = (1 - val)*in[i]; + } +} + +static int l_init(lua_State *s) +{ + ml_channel *dst,*src,*map_dst,*map_src = NULL; + float val = 0; + + if (lua_isuserdata(s,1)) + { + map_src = mutil_get_channel(s,1); + } + else + { + val = luaL_checknumber(s,1); + } + + src = mutil_get_channel(s,2); + + struct pan *pan = calloc(1,sizeof(*pan)); + pan->val = val; + pan->map = map_src != NULL; + + ml_node *node = ml_create_node(proc,pan); + ml_label_node(node,"pan"); + + dst = ml_add_input(node); + pan->l_out = ml_add_output(node); + pan->r_out = ml_add_output(node); + + ml_connect(src,dst); + + if (pan->map) + { + map_dst = ml_add_input(node); + ml_connect(map_src,map_dst); + } + + *(void**) lua_newuserdata(s,sizeof(void*)) = pan; + luaL_setmetatable(s,PAN_TYPENAME); + + return 1; +} + +static int l_left(lua_State *s) +{ + struct pan *pan = *(void**) luaL_checkudata(s,1,PAN_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = pan->l_out; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +static int l_right(lua_State *s) +{ + struct pan *pan = *(void**) luaL_checkudata(s,1,PAN_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = pan->r_out; + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +static int l_set(lua_State *s) +{ + struct pan *pan = *(void**) luaL_checkudata(s,1,PAN_TYPENAME); + pan->val = luaL_checknumber(s,2); + return 0; +} + +int luaopen_melisma_pan(lua_State *s) +{ + ml_check_version(); + +/*** Panning + + local mpan = require 'melisma.pan' + local mexpression = require 'melisma.expression' + local moscil = require 'melisma.oscil' + + local wt = moscil.wavetable.sin() + local lfo_oscil = moscil.init(wt,1,1) + local lfo = (1 + mexpression.input(lfo_oscil,0) / 2):build() + + local oscil = moscil.init(wt,440,0.5) + local pan = mpan.init(lfo,oscil) + + local left = pan:left() + local right = pan:right() + +@classmod pan +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Initialize a panning unit. + +A panning value ranges from 0 to 1, where a value of 0 +will pan everything to the left. + +@static +@function init +@tparam number|channel x Constant pan or a controlling signal. +@tparam channel sig Input signal. +@treturn pan The newly created panning unit. +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Get the left output signal. +@function left +@treturn channel +*/ + lua_pushcfunction(s,l_left); + lua_setfield(s,tbl,"left"); + + +/*** Get the right output signal. +@function right +@treturn channel +*/ + lua_pushcfunction(s,l_right); + lua_setfield(s,tbl,"right"); + +/*** Set the panning value. +@function set +@tparam number x +*/ + lua_pushcfunction(s,l_set); + lua_setfield(s,tbl,"set"); + + luaL_newmetatable(s,PAN_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/passthrough.c b/mod/passthrough.c new file mode 100644 index 0000000..775c6fe --- /dev/null +++ b/mod/passthrough.c @@ -0,0 +1,82 @@ +/* + * 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 "melisma.h" +#include "mutil.h" + +#define PASSTHROUGH_TYPENAME "passthrough" + +static void proc(const ml_proc_args *args) +{ + assert(args->icnt == 1); + assert(args->ocnt == 1); + memcpy(args->out[0],args->in[0],args->nframes*sizeof(float)); +} + +static int l_init(lua_State *s) +{ + ml_channel *src = mutil_get_channel(s,1); + + ml_node *node = ml_create_node(proc,NULL); + ml_label_node(node,"passthrough"); + + ml_channel *dst = ml_add_input(node); + ml_connect(src,dst); + + (void) ml_add_output(node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = node; + luaL_setmetatable(s,PASSTHROUGH_TYPENAME); + + return 1; +} + +static int l_out(lua_State *s) +{ + ml_node *node = *(void**) luaL_checkudata(s,1,PASSTHROUGH_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_passthrough(lua_State *s) +{ + ml_check_version(); + + lua_newtable(s); + int tbl = lua_gettop(s); + + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,PASSTHROUGH_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/playback.c b/mod/playback.c new file mode 100644 index 0000000..f9d22ca --- /dev/null +++ b/mod/playback.c @@ -0,0 +1,342 @@ +/* + * 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 "melisma.h" +#include "mutil.h" +#include "sample.h" + +#define PLAYBACK_TYPENAME "playback" + +struct playback +{ + ml_node *node; + struct sample *smp; + double speed; + double phase; + bool active; + bool loop; + int fn; + int arg; +}; + +static void proc(const ml_proc_args *args) +{ + struct playback *pb = args->userp; + + if (pb->smp == NULL) + { + memset(args->out[0],0,sizeof(*args->out[0])*args->nframes); + return; + } + + unsigned long sr = ml_sample_rate(); + double phase = pb->phase; + float *dst = args->out[0]; + size_t len = pb->smp->len; + float *data = pb->smp->data; + + for (size_t i = 0; i < args->nframes; i++) + { + if (!pb->active) + { + memset(&dst[i],0,(args->nframes - i)*sizeof(*dst)); + return; + } + + float t = phase*len; + + size_t i0 = (size_t) t; + size_t i1 = i0 + 1; + float ip = t - i0; + + i0 %= len; + i1 %= len; + + float s0 = data[i0]; + float s1 = data[i1]; + + dst[i] = s0 + ip*(s1 - s0); + + phase += pb->speed/len; + + while (phase > 1) + { + phase -= 1; + + if (pb->fn != LUA_NOREF) + { + ml_schedule_event(0,pb->fn,pb->arg); + + } + + if (!pb->loop) + { + pb->active = false; + pb->phase = 0; + } + } + + while (phase < 0) + { + phase += 1; + + if (pb->fn != LUA_NOREF) + { + ml_schedule_event(0,pb->fn,pb->arg); + + } + + if (!pb->loop) + { + pb->active = false; + pb->phase = 0; + } + } + } + + pb->phase = phase; +} + +static int l_init(lua_State *s) +{ + struct sample *smp = NULL; + + if (!lua_isnoneornil(s,1)) + { + smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME); + } + + struct playback *pb = calloc(1,sizeof(*pb)); + + pb->smp = smp; + pb->speed = 1; + pb->fn = LUA_NOREF; + pb->arg = LUA_NOREF; + pb->node = ml_create_node(proc,pb); + ml_label_node(pb->node,"playback (%s)",smp ? smp->path : "NULL"); + + ml_add_output(pb->node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = pb; + luaL_setmetatable(s,PLAYBACK_TYPENAME); + + return 1; +} + +static int l_set_sample(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + struct sample *smp = *(void**) luaL_checkudata(s,2,SAMPLE_TYPENAME); + pb->smp = smp; + return 0; +} + +static int l_play(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + pb->active = true; + return 0; +} + +static int l_pause(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + pb->active = false; + return 0; +} + +static int l_reset(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + pb->phase = 0; + return 0; +} + +static int l_stop(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + pb->phase = 0; + pb->active = false; + return 0; +} + +static int l_speed(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + + if (!lua_isnil(s,2)) + { + pb->speed = luaL_checknumber(s,2); + } + + lua_pushnumber(s,pb->speed); + return 1; +} + +static int l_loop(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + + if (!lua_isnil(s,2)) + { + luaL_checktype(s,2,LUA_TBOOLEAN); + pb->loop = lua_toboolean(s,2); + } + + lua_pushboolean(s,pb->loop); + return 1; +} + +static int l_phase(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + + if (!lua_isnil(s,2)) + { + double phase = luaL_checknumber(s,2); + + if (phase > 1) + { + phase = 1; + } + + if (phase < 0) + { + phase = 0; + } + + pb->phase = phase; + } + + lua_pushnumber(s,pb->phase); + return 1; +} + +static int l_out(lua_State *s) +{ + struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(pb->node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +int luaopen_melisma_playback(lua_State *s) +{ +/*** Audio sample playback. + + local msample = require 'melisma.sample' + local mplayback = require 'melisma.playback' + + local sample = msample.load('sample.wav') + local playb = mplayback.init(sample) + + playb:loop(true) + playb:spped(2) + playb:play() + +@classmod playback +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Intialize a playback unit. +@static +@function init +@tparam sample|nil smp +@treturn playback +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Set playback sample. +@function set_sample +@tparam sample smp +*/ + lua_pushcfunction(s,l_set_sample); + lua_setfield(s,tbl,"set_sample"); + +/*** Start playback. +@function play +*/ + lua_pushcfunction(s,l_play); + lua_setfield(s,tbl,"play"); + +/*** Pause playback. +@function pause +*/ + lua_pushcfunction(s,l_pause); + lua_setfield(s,tbl,"pause"); + +/*** Stop playback. + +Equivalent to @{playback.pause}; @{playback.reset}. + +@function stop +*/ + lua_pushcfunction(s,l_stop); + lua_setfield(s,tbl,"stop"); + + +/*** Reset playback. + +Equivalent to @{playback.phase}(0). + +@function reset +*/ + lua_pushcfunction(s,l_reset); + lua_setfield(s,tbl,"reset"); + +/*** Set playback speed. + +A negative speed will play the sample in reverse. + +@function reset +*/ + lua_pushcfunction(s,l_speed); + lua_setfield(s,tbl,"speed"); + +/*** Enable/disable automatic looping. +@function loop +@tparam bool b +*/ + lua_pushcfunction(s,l_loop); + lua_setfield(s,tbl,"loop"); + +/*** Set playback phase. +@function phase +@tparam number x Phase ranging from 0 to 1. +*/ + lua_pushcfunction(s,l_phase); + lua_setfield(s,tbl,"phase"); + + lua_pushcfunction(s,l_out); + lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION); + + luaL_newmetatable(s,PLAYBACK_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/sample.c b/mod/sample.c new file mode 100644 index 0000000..6a3a24b --- /dev/null +++ b/mod/sample.c @@ -0,0 +1,180 @@ +/* + * 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 "sample.h" + +static struct sample *alloc_sample(size_t len) +{ + struct sample *s = malloc(sizeof(*s) + len*sizeof(s->data[0])); + s->len = len; + return s; +} + +static struct sample *resample(struct sample *old, unsigned long from, unsigned long to) +{ + double ratio = (double)to / (double)from; + size_t new_len = ratio * old->len; + struct sample *new = alloc_sample(new_len); + + SRC_DATA src_data = { + .data_in = old->data, + .data_out = new->data, + .input_frames = old->len, + .output_frames = new->len, + .src_ratio = ratio, + }; + + src_simple(&src_data,SRC_SINC_MEDIUM_QUALITY,1); + + new->path = old->path; + free(old); + + return new; +} + +static int l_load(lua_State *s) +{ + const char *path = luaL_checkstring(s,1); + + SF_INFO info; + SNDFILE *f; + + f = sf_open(path,SFM_READ,&info); + + if (f == NULL) + { + luaL_error(s,"failed to open audio file '%s': %s",path,sf_strerror(f)); + } + + size_t len = info.frames*info.channels; + float *data = malloc(sizeof(*data)*len); + + size_t off = 0; + while (off < len) + { + ssize_t res = sf_read_float(f,&data[off],len - off); + + if (res < 0) + { + luaL_error(s,"failed to open audio file '%s': %s",path,sf_strerror(f)); + } + + off += res; + } + + sf_close(f); + + size_t samplerate = ml_sample_rate(); + + for (size_t c = 0; c < info.channels; c++) + { + struct sample *smp = alloc_sample(info.frames); + smp->path = strdup(path); + + for (size_t i = 0; i < info.frames; i++) + { + smp->data[i] = data[i*info.channels + c]; + } + + if (info.samplerate != samplerate) + { + smp = resample(smp,info.samplerate,samplerate); + } + + *(void**) lua_newuserdata(s,sizeof(void*)) = smp; + luaL_setmetatable(s,SAMPLE_TYPENAME); + } + + free(data); + + return info.channels; +} + +static int l_length(lua_State *s) +{ + struct sample *smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME); + unsigned long sr = ml_sample_rate(); + double l = smp->len / (double)sr; + lua_pushnumber(s,l); + return 1; +} + +static int l_path(lua_State *s) +{ + struct sample *smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME); + lua_pushstring(s,smp->path); + return 1; +} + +int luaopen_melisma_sample(lua_State *s) +{ +/*** Load audio samples. + + local msample = require 'melisma.sample' + + local smp = msample.load('sample.wav') + + print(smp:length()) + print(smp:path()) + +@classmod sample +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Load an audio sample. + +If necessary, the audio will be resampled to the current sample rate. + +@static +@function load +@tparam string path +@treturn sample +*/ + lua_pushcfunction(s,l_load); + lua_setfield(s,tbl,"load"); + +/*** Get sample length. +@function length +@treturn number The length of the sample in seconds. +*/ + lua_pushcfunction(s,l_length); + lua_setfield(s,tbl,"length"); + +/*** Get sample path. +@function path +@treturn string The path that the sample was loaded from. +*/ + lua_pushcfunction(s,l_path); + lua_setfield(s,tbl,"path"); + + luaL_newmetatable(s,SAMPLE_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/sample.h b/mod/sample.h new file mode 100644 index 0000000..af2ec83 --- /dev/null +++ b/mod/sample.h @@ -0,0 +1,33 @@ +/* + * 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 . + */ + +#ifndef SAMPLE_H_INCLUDED +#define SAMPLE_H_INCLUDED + +#include + +#define SAMPLE_TYPENAME "audio sample" + +struct sample +{ + char *path; + size_t len; + float data[]; +}; + +#endif diff --git a/mod/serial.c b/mod/serial.c new file mode 100644 index 0000000..a7b51db --- /dev/null +++ b/mod/serial.c @@ -0,0 +1,511 @@ +/* + * 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 +#include +#include +#include "melisma.h" +#include "melisma/ring.h" + +#define SERIAL_TYPENAME "serial" +#define DEFAULT_BAUD 9600 +#define DEFAULT_FORMAT "8N1" +#define DEFAULT_ENCODING "cstring" +#define DEFAULT_BUFSIZE 4096 + +struct serial; + +typedef int (*decode_fn)(struct serial*); + +struct conf +{ + char *path; + char *format; + int datasize; + int stopbits; + int parity; + int baud; + int baudflag; + decode_fn decode; + int bufsize; + int callb; +}; + +struct serial +{ + struct conf conf; + int fd; + int synced; + size_t len; + size_t cap; + uint8_t *data; + uint8_t *tmpdata; + ml_ring *ring; + lua_State *state; +}; + +static int cstring(struct serial*); + +static const struct { + decode_fn decode; + const char *name; +} encoding_map[] = { + { cstring, "cstring" }, + { NULL, NULL }, +}; + +static void free_conf(struct conf *c) +{ + free(c->path); + free(c->format); +} + +static void put_byte(struct serial *se, int c) +{ + if (se->len == se->cap) + { + se->cap = se->cap == 0 ? 32 : se->cap*2; + se->data = realloc(se->data,se->cap); + se->tmpdata = realloc(se->tmpdata,se->cap); + } + se->data[se->len++] = c; +} + +static void io_handler(int fd, int events, void *args) +{ + struct serial *se = args; + if (se->conf.decode(se) == 0) + { + if (ml_ring_has_space(se->ring,se->len + sizeof se->len)) + { + ml_ring_write_blocking(se->ring,&se->len,sizeof se->len); + ml_ring_write_blocking(se->ring,se->data,se->len); + } + else + { + fprintf(stderr,"dropped %lu bytes\n",se->len); + } + } + else + { + fprintf(stderr,"failed to decode message\n"); + } + se->len = 0; +} + +static void read_conf(lua_State *s, int tbl, struct conf *c) +{ + int top = lua_gettop(s); + lua_getfield(s,tbl,"path"); + + if (lua_isnil(s,-1)) + { + free_conf(c); + luaL_error(s,"missing 'path' field"); + } + + if (!lua_isstring(s,-1)) + { + free_conf(c); + luaL_error(s,"'path' field is not a string"); + } + + c->path = strdup(lua_tostring(s,-1)); + lua_settop(s,top); + + + lua_getfield(s,tbl,"baud"); + + if (lua_isnil(s,-1)) + { + c->baud = DEFAULT_BAUD; + } + else if (lua_isinteger(s,-1)) + { + c->baud = lua_tointeger(s,-1); + } + else + { + free_conf(c); + luaL_error(s,"'baud' field is not an integer"); + } + + switch (c->baud) + { + case 0: c->baudflag = B0; break; + case 50: c->baudflag = B50; break; + case 75: c->baudflag = B75; break; + case 110: c->baudflag = B110; break; + case 134: c->baudflag = B134; break; + case 150: c->baudflag = B150; break; + case 200: c->baudflag = B200; break; + case 300: c->baudflag = B300; break; + case 600: c->baudflag = B600; break; + case 1200: c->baudflag = B1200; break; + case 1800: c->baudflag = B1800; break; + case 2400: c->baudflag = B2400; break; + case 4800: c->baudflag = B4800; break; + case 9600: c->baudflag = B9600; break; + case 19200: c->baudflag = B19200; break; + case 38400: c->baudflag = B38400; break; + case 57600: c->baudflag = B57600; break; + case 115200: c->baudflag = B115200; break; + case 230400: c->baudflag = B230400; break; + case 460800: c->baudflag = B460800; break; + case 500000: c->baudflag = B500000; break; + case 576000: c->baudflag = B576000; break; + case 921600: c->baudflag = B921600; break; + case 1000000: c->baudflag = B1000000; break; + case 1152000: c->baudflag = B1152000; break; + case 1500000: c->baudflag = B1500000; break; + case 2000000: c->baudflag = B2000000; break; + default: c->baudflag = -1; break; + } + + if (c->baudflag < 0) + { + free_conf(c); + luaL_error(s,"unsupported baud rate %d",c->baud); + } + + lua_settop(s,top); + + const char *format; + lua_getfield(s,tbl,"format"); + + if (lua_isnil(s,-1)) + { + format = DEFAULT_FORMAT; + } + else if (lua_isstring(s,-1)) + { + format = lua_tostring(s,-1); + } + else + { + free_conf(c); + luaL_error(s,"'format' field is not a string"); + } + + c->format = strdup(format); + + if (strlen(format) != 3 || + !strchr("5678",format[0]) || + !strchr("NEOneo",format[1]) || + !strchr("12",format[2])) + { + free_conf(c); + luaL_error(s,"invalid frame format '%s'",format); + } + + c->datasize = format[0] - '0'; + c->parity = toupper(format[1]); + c->stopbits = format[2] - '0'; + + lua_settop(s,top); + + lua_getfield(s,tbl,"encoding"); + const char *encoding = NULL; + + if (lua_isnil(s,-1)) + { + encoding = DEFAULT_ENCODING; + } + else if (lua_isstring(s,-1)) + { + encoding = lua_tostring(s,-1); + } + else + { + free_conf(c); + luaL_error(s,"'encoding' field is not a string"); + } + + for (size_t i = 0;; i++) + { + c->decode = encoding_map[i].decode; + const char *name = encoding_map[i].name; + + if (c->decode == NULL) + { + free_conf(c); + luaL_error(s,"unknown encoding '%s'",encoding); + } + + if (strcmp(name,encoding) == 0) + { + break; + } + } + + lua_getfield(s,tbl,"bufsize"); + + if (lua_isnil(s,-1)) + { + c->bufsize = DEFAULT_BUFSIZE; + } + else if (lua_isinteger(s,-1)) + { + c->bufsize = lua_tointeger(s,-1); + + if (c->bufsize <= 0) + { + luaL_error(s,"invalid bufsize %d",c->bufsize); + } + } + else + { + free_conf(c); + luaL_error(s,"'bufsize' is not an integer"); + } + + lua_getfield(s,tbl,"callback"); + + if (lua_isnil(s,-1)) + { + c->callb = LUA_NOREF; + } + else if (lua_isfunction(s,-1)) + { + c->callb = luaL_ref(s,LUA_REGISTRYINDEX); + } + else + { + free_conf(c); + luaL_error(s,"'callback' is not a function"); + } +} + +static void setup_tty(struct termios *t, struct conf *c) +{ + cfsetispeed(t,c->baudflag); + + switch (c->stopbits) + { + case 1: t->c_cflag &= ~CSTOPB; break; + case 2: t->c_cflag |= CSTOPB; break; + default: assert(0); + } + + switch (c->parity) + { + case 'N': + case 'n': + t->c_cflag &= ~PARENB; + break; + case 'O': + case 'o': + t->c_cflag |= PARENB; + t->c_cflag |= PARODD; + break; + case 'E': + case 'e': + t->c_cflag |= PARENB; + t->c_cflag &= ~PARODD; + break; + default: + assert(0); + } + + t->c_cflag &= ~CSIZE; + + switch (c->datasize) + { + case 5: t->c_cflag |= CS5; break; + case 6: t->c_cflag |= CS6; break; + case 7: t->c_cflag |= CS7; break; + case 8: t->c_cflag |= CS8; break; + default: assert(0); + } + + t->c_cflag |= CREAD|CLOCAL; + t->c_cflag &= ~CRTSCTS; + t->c_lflag &= ~(ICANON|ECHO|ISIG); + t->c_iflag &= ~(IXON|IXOFF|IXANY); + t->c_oflag &= ~OPOST; + t->c_cc[VMIN] = 1; + t->c_cc[VTIME] = 0; +} + +static void proc(const ml_proc_args *args) +{ + struct serial *se = args->userp; + size_t len; + + while (ml_ring_read(se->ring,&len,sizeof len) == sizeof len) + { + ml_ring_read_blocking(se->ring,se->tmpdata,len); + + if (se->conf.callb != LUA_NOREF) + { + lua_pushlstring(se->state,(const char*)se->tmpdata,len); + int arg = luaL_ref(se->state,LUA_REGISTRYINDEX); + ml_schedule_event(0,se->conf.callb,arg); + } + } +} + +static int l_open(lua_State *s) +{ + luaL_checktype(s,1,LUA_TTABLE); + + struct conf conf = {0}; + + read_conf(s,1,&conf); + + int fd = open(conf.path,O_RDONLY|O_NOCTTY); + + if (fd < 0) + { + luaL_error(s,"failed to open %s: %s",conf.path,strerror(errno)); + } + + struct termios tty = {0}; + tcgetattr(fd,&tty); + + setup_tty(&tty,&conf); + + tcsetattr(fd,TCSANOW,&tty); + tcflush(fd,TCIFLUSH); + + struct serial *se = calloc(1,sizeof(*se)); + se->fd = fd; + se->conf = conf; + se->ring = ml_ring_init(conf.bufsize); + se->state = s; + + *(void**) lua_newuserdata(s,sizeof(void**)) = se; + luaL_setmetatable(s,SERIAL_TYPENAME); + + ml_add_io_handler(fd,POLLIN,io_handler,se); + + ml_node *node = ml_create_node(proc,se); + ml_channel *src = ml_add_output(node); + ml_channel *dst = ml_add_input(ml_get_root_node()); + ml_connect(src,dst); + ml_label_node(node,"serial %s",conf.path); + + return 1; +} + +static int read_byte(struct serial *se) +{ + uint8_t c; + + if (read(se->fd,&c,sizeof c) < sizeof c) + { + return -1; + } + + return c; +} + +static int sync_(struct serial *se) +{ + int c; + + if (se->synced) + { + return 0; + } + + while ((c = read_byte(se)) != 0) + { + if (c < 0) + { + return -1; + } + } + + se->synced = 1; + return 0; +} + +static int cstring(struct serial *se) +{ + if (sync_(se) < 0) + { + return -1; + } + + int c; + + while ((c = read_byte(se)) != 0) + { + if (c < 0) + { + se->synced = 0; + return -1; + } + + put_byte(se,c); + } + + return 0; +} + +int luaopen_melisma_serial(lua_State *s) +{ +/*** Read data on serial ports. + + local mserial = require 'melisma.serial' + + mserial.open { + path = '/dev/ttyACM0', + callback = print, + } + +@classmod serial +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Open a serial port. + +`conf` is a table containing the following keys: + +- `path` (**string, required**): Path to serial port (e.g. `/dev/ttyACM0`). +- `callback` (**function, required**): Callback function for handling messages. +- `baud` (**integer**); baud rate (default `9600`). +- `format` (**string**): frame format (default `'8N1'`). +- `encoding` (**string**): method for encoding messages (default `'cstring'`). May be one of: + - `'cstring'`: `NULL`-terminated strings. + +@static +@function open +@tparam table conf +@treturn serial +*/ + lua_pushcfunction(s,l_open); + lua_setfield(s,tbl,"open"); + + luaL_newmetatable(s,SERIAL_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_pushvalue(s,tbl); + return 1; +} diff --git a/mod/spring.c b/mod/spring.c new file mode 100644 index 0000000..a6f96ea --- /dev/null +++ b/mod/spring.c @@ -0,0 +1,173 @@ +/* + * 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 "melisma.h" + +#define SPRING_TYPENAME "spring" + +struct spring +{ + ml_node *node; + float target; + float value; + float freq; + float damping; + float velocity; + bool mapped; +}; + +static void proc(const ml_proc_args *args) +{ + struct spring *spring = args->userp; + + float *out = args->out[0]; + float *mapped = spring->mapped ? args->in[0] : NULL; + float dt = 1.f/ml_sample_rate(); + + for (size_t i = 0; i < args->nframes; i++) + { + float target = mapped ? mapped[i] : spring->target; + float w = 2*M_PI*spring->freq; + float f = (target - spring->value)*w*w; + float d = 2*w*spring->velocity*spring->damping; + float a = f - d; + + spring->velocity += a*dt; + spring->value += spring->velocity*dt; + + out[i] = spring->value; + } +} + +static int l_damping(lua_State *s) +{ + struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME); + float x = luaL_checknumber(s,2); + spring->damping = x; + return 0; +} + +static int l_freq(lua_State *s) +{ + struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME); + float x = luaL_checknumber(s,2); + spring->freq = x; + return 0; +} + +static int l_target(lua_State *s) +{ + struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME); + float x = luaL_checknumber(s,2); + spring->target = x; + return 0; +} + +static int l_map(lua_State *s) +{ + struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME); + + if (spring->mapped) + { + luaL_error(s,"already mapped"); + } + + ml_channel *src = *(void**) luaL_checkudata(s,2,MELISMA_OUTPUT_CHANNEL_TYPENAME); + ml_channel *dst = ml_add_input(spring->node); + ml_connect(src,dst); + + spring->mapped = true; + + return 0; +} + +static int l_sample(lua_State *s) +{ + struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME); + lua_pushnumber(s,spring->value); + return 1; +} + +static int l_call(lua_State *s) +{ + struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME); + *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(spring->node,0); + luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME); + return 1; +} + +static int l_init(lua_State *s) +{ + float value = luaL_checknumber(s,1); + float freq = luaL_checknumber(s,2); + float damping = luaL_checknumber(s,3); + + struct spring *spring = calloc(1,sizeof(*spring)); + spring->value = value; + spring->freq = freq; + spring->damping = damping; + spring->mapped = false; + + spring->node = ml_create_node(proc,spring); + ml_add_output(spring->node); + + *(void**) lua_newuserdata(s,sizeof(void*)) = spring; + luaL_setmetatable(s,SPRING_TYPENAME); + + return 1; +} + +int luaopen_melisma_spring(lua_State *s) +{ + lua_newtable(s); + int tbl = lua_gettop(s); + + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + + lua_pushcfunction(s,l_freq); + lua_setfield(s,tbl,"freq"); + + lua_pushcfunction(s,l_damping); + lua_setfield(s,tbl,"damping"); + + lua_pushcfunction(s,l_target); + lua_setfield(s,tbl,"target"); + + lua_pushcfunction(s,l_sample); + lua_setfield(s,tbl,"sample"); + + lua_pushcfunction(s,l_map); + lua_setfield(s,tbl,"map"); + + luaL_newmetatable(s,SPRING_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_pushcfunction(s,l_call); + lua_setfield(s,mt,"__call"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/timer.c b/mod/timer.c new file mode 100644 index 0000000..92659dd --- /dev/null +++ b/mod/timer.c @@ -0,0 +1,152 @@ +/* + * 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 "melisma.h" + +#define TIMER_TYPENAME "timer" + +struct timer +{ + bool on; + uint64_t t; +}; + +static void proc(const ml_proc_args *args) +{ + struct timer *tmr = args->userp; + + if (tmr->on) + { + tmr->t += args->nframes; + } +} + +static int l_init(lua_State *s) +{ + struct timer *tmr = calloc(1,sizeof(*tmr)); + + tmr->on = false; + tmr->t = 0; + + ml_node *node = ml_create_node(proc,tmr); + ml_channel *src = ml_add_output(node); + ml_channel *dst = ml_add_input(ml_get_root_node()); + ml_connect(src,dst); + + *(void**) lua_newuserdata(s,sizeof(void*)) = tmr; + luaL_setmetatable(s,TIMER_TYPENAME); + + return 1; +} + +static int l_start(lua_State *s) +{ + struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME); + tmr->on = true; + return 0; +} + +static int l_stop(lua_State *s) +{ + struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME); + tmr->on = false; + return 0; +} + +static int l_reset(lua_State *s) +{ + struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME); + tmr->t = 0; + return 0; +} + +static int l_read(lua_State *s) +{ + struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME); + double t = (double) tmr->t / (double) ml_sample_rate(); + lua_pushnumber(s,t); + return 1; +} + +int luaopen_melisma_timer(lua_State *s) +{ +/*** Time measurement. + + local mtimer = require 'melisma.timer' + local mkeycallback = require 'melisma.keycallback' + + local timer = mtimer.init() + timer:start() + + mkeycallback.set(function() + print(timer:read()) + timer:reset() + end) + +@classmod timer +*/ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** Create a new timer. +@static +@function init +@treturn timer +*/ + lua_pushcfunction(s,l_init); + lua_setfield(s,tbl,"init"); + +/*** Start timer. +@function start +*/ + lua_pushcfunction(s,l_start); + lua_setfield(s,tbl,"start"); + +/*** Stop timer. +@function stop +*/ + lua_pushcfunction(s,l_stop); + lua_setfield(s,tbl,"stop"); + +/*** Reset timer. +@function reset +*/ + lua_pushcfunction(s,l_reset); + lua_setfield(s,tbl,"reset"); + + +/*** Read timer. +@function read +@treturn number Elapsed seconds. +*/ + lua_pushcfunction(s,l_read); + lua_setfield(s,tbl,"read"); + + luaL_newmetatable(s,TIMER_TYPENAME); + int mt = lua_gettop(s); + + lua_pushvalue(s,tbl); + lua_setfield(s,mt,"__index"); + + lua_settop(s,tbl); + return 1; +} diff --git a/mod/udp.c b/mod/udp.c new file mode 100644 index 0000000..6448a56 --- /dev/null +++ b/mod/udp.c @@ -0,0 +1,391 @@ +/* + * 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 +#include +#include +#include + +#include + +#include "melisma.h" +#include "melisma/ring.h" + +#define SOCKADDR_TYPENAME "sockaddr" +#define UDP_SOCKET_TYPENAME "udp socket" +#define UDP_PACKET_SIZE 65536 + +struct udp_socket +{ + int fd; + int family; + int event; + int bound; + int callb; + lua_State *state; + ml_ring *recvring; + ml_ring *sendring; + uint8_t pollbuf[UDP_PACKET_SIZE]; + uint8_t procbuf[UDP_PACKET_SIZE]; +}; + +// TODO: CHECK BUFFER SIZES FOR ADDRESSES + +static int l_sockaddr_unix(lua_State *s) +{ + size_t len; + const char *path = luaL_checklstring(s,1,&len); + + struct sockaddr_storage *sa = lua_newuserdata(s,sizeof *sa); + luaL_setmetatable(s,SOCKADDR_TYPENAME); + memset(sa,0,sizeof *sa); + + ((struct sockaddr_un*)sa)->sun_family = AF_UNIX; + memcpy(((struct sockaddr_un*)sa)->sun_path,path,len); + + return 1; +} + +static int l_sockaddr_inet(lua_State *s) +{ + const char *addr = luaL_checkstring(s,1); + int port = luaL_checkinteger(s,2); + + struct in_addr in_addr; + if (inet_pton(AF_INET,addr,&in_addr) != 1) + { + luaL_error(s,"invalid ipv4 address '%s'",addr); + } + + struct sockaddr_storage *sa = lua_newuserdata(s,sizeof *sa); + luaL_setmetatable(s,SOCKADDR_TYPENAME); + memset(sa,0,sizeof *sa); + + ((struct sockaddr_in*)sa)->sin_family = AF_INET; + ((struct sockaddr_in*)sa)->sin_port = htons(port); + memcpy(&((struct sockaddr_in*)sa)->sin_addr,&in_addr,sizeof in_addr); + + return 1; +} + +static void recv_handler(int fd, int flags, void *args) +{ + struct udp_socket *us = args; + ssize_t len = recvfrom(fd,us->pollbuf,sizeof us->pollbuf,0,NULL,NULL); + + if (len < 0) + { + perror("recvfrom"); + return; + } + + if (!ml_ring_has_space(us->recvring,len + sizeof len)) + { + fprintf(stderr,"dropped udp packet"); + } + + ml_ring_write_blocking(us->recvring,&len,sizeof len); + ml_ring_write_blocking(us->recvring,us->pollbuf,len); +} + +static void send_handler(int fd, int flags, void *args) +{ + struct udp_socket *us = args; + + size_t sa_len,len; + struct sockaddr_storage storage = {0}; + + ml_ring_read_blocking(us->sendring,&sa_len,sizeof sa_len); + ml_ring_read_blocking(us->sendring,&storage,sa_len); + ml_ring_read_blocking(us->sendring,&len,sizeof len); + ml_ring_read_blocking(us->sendring,us->pollbuf,len); + + if (sendto(us->fd,us->pollbuf,len,0,(struct sockaddr*) &storage,sa_len) < 0) + { + perror("sendto"); + } + + uint64_t x; + if (read(us->event,&x,sizeof x) != sizeof x) + ; +} + +static int l_socket(lua_State *s) +{ + int family = lua_tointeger(s,lua_upvalueindex(1)); + int fd = socket(family,SOCK_DGRAM,0); + + if (fd < 0) + { + const char *err = strerror(errno); + luaL_error(s,"failed to create socket: %s",err); + } + + struct udp_socket *us = calloc(1,sizeof(*us)); + us->fd = fd; + us->family = family; + us->sendring = ml_ring_init(UDP_PACKET_SIZE); + us->event = eventfd(0,EFD_SEMAPHORE); + + ml_add_io_handler(us->event,POLLIN,send_handler,us); + + *(struct udp_socket**) lua_newuserdata(s,sizeof(struct udp_socket*)) = us; + luaL_setmetatable(s,UDP_SOCKET_TYPENAME); + + return 1; +} + +static const char *family_name(int family) +{ + switch (family) + { + case AF_UNIX: return "unix"; + case AF_INET: return "inet"; + default: assert(0); + } +} + +static void proc(const ml_proc_args *args) +{ + struct udp_socket *us = args->userp; + ssize_t len; + + while (!ml_ring_empty(us->recvring)) + { + ml_ring_read_blocking(us->recvring,&len,sizeof len); + ml_ring_read_blocking(us->recvring,us->procbuf,len); + + lua_pushlstring(us->state,(const char*)us->procbuf,len); + int arg = luaL_ref(us->state,LUA_REGISTRYINDEX); + + ml_schedule_event(0,us->callb,arg); + } +} + +static size_t get_sockaddr_len(struct sockaddr *sa) +{ + if (sa->sa_family == AF_UNIX) + { + const char *path = ((struct sockaddr_un*)sa)->sun_path; + + if (path[0] == 0 && path[1] != 0) + { + return offsetof(struct sockaddr_un,sun_path) + strlen(path+1) + 1; + } + + return offsetof(struct sockaddr_un,sun_path) + strlen(path); + } + else + { + return sizeof(struct sockaddr_in); + } +} + +static void check_family(lua_State *s, struct udp_socket *us, struct sockaddr *sa) +{ + if (us->family != sa->sa_family) + { + luaL_error(s,"sockaddr address family (%s) does not match socket address family (%s)", + family_name(sa->sa_family), family_name(us->family)); + } +} + +static int l_bind(lua_State *s) +{ + struct udp_socket *us = *(struct udp_socket**) luaL_checkudata(s,1,UDP_SOCKET_TYPENAME); + struct sockaddr *sa = luaL_checkudata(s,2,SOCKADDR_TYPENAME); + + luaL_checktype(s,3,LUA_TFUNCTION); + + if (us->bound) + { + luaL_error(s,"socket is already bound"); + } + + check_family(s,us,sa); + + size_t len = get_sockaddr_len(sa); + + if (bind(us->fd,sa,len) < 0) + { + const char *err = strerror(errno); + luaL_error(s,"failed to bind socket: %s",err); + } + + us->bound = 1; + us->recvring = ml_ring_init(UDP_PACKET_SIZE); + us->state = s; + + ml_add_io_handler(us->fd,POLLIN,recv_handler,us); + + lua_pushvalue(s,3); + us->callb = luaL_ref(s,LUA_REGISTRYINDEX); + + ml_node *node = ml_create_node(proc,us); + ml_channel *src = ml_add_output(node); + ml_channel *dst = ml_add_input(ml_get_root_node()); + ml_connect(src,dst); + + return 0; +} + +static int l_send(lua_State *s) +{ + struct udp_socket *us = *(struct udp_socket**) luaL_checkudata(s,1,UDP_SOCKET_TYPENAME); + struct sockaddr *sa = luaL_checkudata(s,2,SOCKADDR_TYPENAME); + size_t len; + const char *data = luaL_checklstring(s,3,&len); + + check_family(s,us,sa); + + size_t sa_len = get_sockaddr_len(sa); + size_t full_len = sizeof len + len + sizeof sa_len + sa_len; + + if (!ml_ring_has_space(us->sendring,full_len)) + { + lua_pushboolean(s,0); + return 1; + } + + ml_ring_write_blocking(us->sendring,&sa_len,sizeof sa_len); + ml_ring_write_blocking(us->sendring,sa,sa_len); + ml_ring_write_blocking(us->sendring,&len,sizeof len); + ml_ring_write_blocking(us->sendring,data,len); + + uint64_t x = 1; + if (write(us->event,&x,sizeof x) < sizeof x) + ; + + lua_pushboolean(s,1); + return 1; +} + +int luaopen_melisma_udp(lua_State *s) +{ + lua_newtable(s); + int tbl = lua_gettop(s); + +/*** UDP support. + + local mudp = require 'melisma.udp' + + local addr = mudp.sockaddr.inet('127.0.0.1',8000) + local sock = mudp.socket.inet() + + sock:send(addr,'hello') + sock:bind(addr,print) + +@module udp +*/ + +/*** Socket address description. +@type sockaddr +*/ + lua_newtable(s); + int sockaddr_tbl = lua_gettop(s); + +/*** Create a unix domain address description. +@static +@function unix +@tparam string path +@treturn sockaddr +*/ + lua_pushcfunction(s,l_sockaddr_unix); + lua_setfield(s,sockaddr_tbl,"unix"); + + +/*** Create a internet domain address description. +@static +@function inet +@tparam string host +@tparam integer port +@treturn sockaddr +*/ + lua_pushcfunction(s,l_sockaddr_inet); + lua_setfield(s,sockaddr_tbl,"inet"); + + lua_pushvalue(s,sockaddr_tbl); + lua_setfield(s,tbl,"sockaddr"); + + luaL_newmetatable(s,SOCKADDR_TYPENAME); + int sockaddr_mt = lua_gettop(s); + + lua_pushvalue(s,sockaddr_tbl); + lua_setfield(s,sockaddr_mt,"__index"); + +/*** Handle to a socket. +@type socket +*/ + + lua_newtable(s); + int socket_tbl = lua_gettop(s); + +/*** Create a unix domain socket. +@static +@function unix +@treturn socket +*/ + lua_pushinteger(s,AF_UNIX); + lua_pushcclosure(s,l_socket,1); + lua_setfield(s,socket_tbl,"unix"); + + +/*** Create an internet domain socket. +@static +@function inet +@treturn socket +*/ + lua_pushinteger(s,AF_INET); + lua_pushcclosure(s,l_socket,1); + lua_setfield(s,socket_tbl,"inet"); + +/*** Bind a socket to an address. +@function bind +@tparam sockaddr addr Address description. +@tparam function fn Packet callback function. +*/ + lua_pushcfunction(s,l_bind); + lua_setfield(s,socket_tbl,"bind"); + +/*** Send a packet on a socket. +@function send +@tparam sockaddr addr Address description. +@tparam string pkt Packet to send. +*/ + lua_pushcfunction(s,l_send); + lua_setfield(s,socket_tbl,"send"); + + lua_pushvalue(s,socket_tbl); + lua_setfield(s,tbl,"socket"); + + luaL_newmetatable(s,UDP_SOCKET_TYPENAME); + int socket_mt = lua_gettop(s); + + lua_pushvalue(s,socket_tbl); + lua_setfield(s,socket_mt,"__index"); + + lua_settop(s,tbl); + return 1; +} -- cgit v1.3.1