├── Makefile ├── README ├── app ├── Makefile ├── autoload.lua ├── co.lua ├── evq.lua ├── getopt.lua ├── list.lua ├── log.lua ├── luaposix.c ├── misc.lua ├── sandbox.lua └── strict.lua ├── doc ├── .gitignore ├── Makefile ├── about.txt ├── appregiator.mp3 ├── architecture.txt ├── bin │ ├── libdocs │ └── txt2html ├── concepts.txt ├── dsp.txt ├── example.mp3 ├── examples.txt ├── gui.txt ├── htdocs │ ├── highlight.css │ ├── style.css │ └── zplayer.swf ├── img │ ├── connect-1.png │ ├── connect-2.png │ ├── gui.png │ ├── worp-icon.png │ └── worp-long.png ├── index.txt ├── jack.txt ├── libs.txt ├── live_coding.txt ├── main.txt ├── modules.txt └── worp.svg ├── examples ├── appregiator.lua ├── gen.lua └── scales.lua ├── lib ├── Chord.lua ├── Core.lua ├── Dsp │ ├── Adsr.lua │ ├── Const.lua │ ├── Control.lua │ ├── Filter.lua │ ├── Mod.lua │ ├── Noise.lua │ ├── Osc.lua │ ├── Pan.lua │ ├── Pitchshift.lua │ ├── Reverb.lua │ ├── Saw.lua │ ├── Square.lua │ └── Width.lua ├── Fluidsynth.lua ├── Gui.lua ├── Jack.lua ├── Linuxsampler.lua ├── Makefile ├── Metro.lua ├── Mixer.lua ├── Poly.lua ├── jack_c.c └── jack_c.so ├── test ├── blip.lua ├── filter.lua ├── gen.lua ├── looper.lua ├── pitch-shift.lua ├── reich.lua ├── simple-piano.lua └── simple-synth.lua ├── vimplugin.lua └── worp /Makefile: -------------------------------------------------------------------------------- 1 | 2 | CFLAGS += -Wall -Werror 3 | CFLAGS += -shared -fPIC 4 | CFLAGS += $(shell pkg-config --cflags luajit) 5 | CFLAGS += -g 6 | 7 | LDFLAGS += $(shell pkg-config --libs luajit) 8 | LDFLAGS += -lrt -lcrypt 9 | LDFLAGS += -g 10 | 11 | default: 12 | make all 13 | 14 | %: 15 | make -C app $@ 16 | make -C lib $@ 17 | 18 | export CFLAGS LDFLAGS 19 | 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | 3 | Worp is an experimental sound / music / DSP engine written in LuaJIT, currently 4 | in early alpha state. 5 | 6 | The main Worp web site and documentation can be found at http://worp.zevv.nl 7 | 8 | == Some highlights: 9 | 10 | - Built on LuaJIT: the ease of using a high level language, but getting near 11 | native C performance. Even low-level DSP functions (filters, reverb) can be 12 | coded in Lua, still giving great performance. 13 | 14 | - Live coding: add or update code while the application is running through the 15 | use of a vim plugin. New chunks can be sent and compiled on the fly. 16 | 17 | - Bindings for Fluidsynth and midi support 18 | 19 | 20 | == License 21 | 22 | Copyright (C) 2013 Ico Doornekamp. 23 | 24 | This program is free software; you can redistribute it and/or modify it under 25 | the terms of the GNU General Public License version 2 as published by the Free 26 | Software Foundation. 27 | 28 | -------------------------------------------------------------------------------- /app/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: luaposix.so 3 | 4 | LDFLAGS += -lrt -lcrypt 5 | 6 | %.so: %.c 7 | $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) 8 | 9 | clean: 10 | rm -f *.so 11 | -------------------------------------------------------------------------------- /app/autoload.lua: -------------------------------------------------------------------------------- 1 | 2 | local mtime = {} 3 | 4 | function autoload(id, parent, path) 5 | 6 | local function lookup(env, s) 7 | 8 | -- Check if parent provides 9 | 10 | local v = rawget(parent, s) 11 | if v then return v end 12 | 13 | -- Check if it is a lib we can load 14 | 15 | local fname = "%s/%s.lua" % { path, s } 16 | local stat = P.stat(fname) 17 | if stat and stat.type == "regular" then 18 | logf(LG_DBG, "Loading library %s", fname) 19 | local chunk, err = loadfile(fname, "t", env) 20 | if chunk then 21 | local ok = safecall(chunk) 22 | if not ok then 23 | logf(LG_WRN, "%s", err) 24 | end 25 | else 26 | logf(LG_WRN, "%s", err) 27 | end 28 | local v = rawget(env, s) 29 | if v then return v end 30 | end 31 | 32 | -- Check if lib subdirectory 33 | 34 | local fname = "%s/%s" % { path, s } 35 | local stat = P.stat(fname) 36 | if stat and stat.type == "directory" then 37 | return autoload(path, sandbox.env, "lib/" .. s) 38 | end 39 | 40 | -- Fallback 41 | 42 | return parent[s] 43 | end 44 | 45 | return setmetatable({}, { 46 | __index = function(env, s) 47 | v = lookup(env, s) 48 | env[s] = v 49 | return v 50 | end, 51 | __tostring = function(t) 52 | return "Autoload library %q" % id 53 | end, 54 | }) 55 | 56 | end 57 | 58 | -- vi: ft=lua ts=3 sw=3 59 | -------------------------------------------------------------------------------- /app/co.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Coroutine shortcuts 4 | -- 5 | 6 | function sleep(d) 7 | at(d, resumer()) 8 | coroutine.yield() 9 | end 10 | 11 | 12 | -- 13 | -- Return function that will resume the running coroutine, propagating 14 | -- errors 15 | -- 16 | 17 | function resumer(co) 18 | co = co or coroutine.running() 19 | return function(...) 20 | local function aux(ok, ...) 21 | if ok then 22 | return ... 23 | else 24 | print(... .. debug.traceback(co, "", 1), 0) 25 | end 26 | end 27 | return aux(coroutine.resume(co, ...)) 28 | end 29 | end 30 | 31 | -- vi: ft=lua ts=3 sw=3 32 | -------------------------------------------------------------------------------- /app/evq.lua: -------------------------------------------------------------------------------- 1 | 2 | t_now = 0 3 | 4 | local evs = {} 5 | local fds = {} 6 | local t_start 7 | local running = true 8 | 9 | -- 10 | -- Schedule function 'fn' to be called in 't' seconds. 'fn' can be as string, 11 | -- which will be resolved in the 'env' table at calling time 12 | -- 13 | 14 | function at(t, fn, ...) 15 | local ev = { 16 | t_when = t_now + t, 17 | fn = fn, 18 | args = { ... } 19 | } 20 | table.insert(evs, ev) 21 | table.sort(evs, function(a, b) 22 | return a.t_when > b.t_when 23 | end) 24 | end 25 | 26 | 27 | -- 28 | -- Play a note for the given duration using the given sound generator 29 | -- 30 | 31 | function play(fn, note, vol, dur) 32 | fn(note, vol or 127) 33 | at(dur * 0.99, function() 34 | fn(note, 0) 35 | end) 36 | end 37 | 38 | 39 | -- 40 | -- Return monotonic time, starts at zero at first invocation 41 | -- 42 | 43 | function time() 44 | local s, ns = P.clock_gettime(P.CLOCK_MONOTONIC) 45 | local t = s + ns / 1e9 46 | t_start = t_start or t 47 | return t - t_start 48 | end 49 | 50 | 51 | -- 52 | -- Register the given file descriptor to the main poll() looP. 'fn' is called 53 | -- when new data is available on the fd 54 | -- 55 | 56 | function watch_fd(fd, fn) 57 | assert(fn) 58 | assert(type(fn) == "function") 59 | fds[fd] = { events = { IN = true }, fn = fn } 60 | end 61 | 62 | 63 | -- 64 | -- Exit event loop 65 | -- 66 | 67 | local stop_list = {} 68 | 69 | function stop() 70 | for fn in pairs(stop_list) do 71 | fn() 72 | end 73 | running = false 74 | end 75 | 76 | 77 | function on_stop(fn) 78 | stop_list[fn] = true 79 | end 80 | 81 | 82 | P.signal(P.SIGINT, function() 83 | print("") 84 | running = false 85 | end) 86 | 87 | 88 | -- 89 | -- Run the main event loop, scheduling timers and handling file descriptors 90 | -- 91 | 92 | function mainloop() 93 | 94 | while running do 95 | 96 | local dt = 10 97 | local ev = evs[#evs] 98 | 99 | if ev then 100 | dt = math.min(ev.t_when - time()) 101 | end 102 | 103 | if dt > 0 then 104 | 105 | local fds2 = {} 106 | for k, v in pairs(fds) do fds2[k] = v end 107 | 108 | local r, a = P.poll(fds2, dt * 1000) 109 | if r and r > 0 then 110 | for fd in pairs(fds2) do 111 | if fds2[fd].revents and fds[fd].revents.IN then 112 | t_now = time() 113 | local ok, rv = safecall(fds2[fd].fn) 114 | if not ok or rv == false then 115 | fds[fd] = nil 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | while ev and time() > ev.t_when do 123 | table.remove(evs) 124 | t_now = ev.t_when 125 | safecall(ev.fn, unpack(ev.args)) 126 | ev = evs[#evs] 127 | end 128 | 129 | end 130 | end 131 | 132 | -- vi: ft=lua ts=3 sw=3 133 | -------------------------------------------------------------------------------- /app/getopt.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Simple getopt 4 | -- 5 | 6 | function getopt(arg_in, optstring) 7 | 8 | local opt_out = {} 9 | local arg_out = {} 10 | 11 | while arg_in[1] do 12 | local char, val = string.match(arg_in[1], "^-(.)(.*)") 13 | if char then 14 | local found, needarg = string.match(optstring, "(" ..char .. ")(:?)") 15 | if not found then 16 | print("Invalid option '%s'\n" % char) 17 | return nil 18 | end 19 | if needarg == ":" then 20 | if not val or string.len(val)==0 then 21 | table.remove(arg, 1) 22 | val = arg_in[1] 23 | end 24 | if not val then 25 | print("option '%s' requires an argument\n" % char) 26 | return nil 27 | end 28 | else 29 | val = true 30 | end 31 | opt_out[char] = val 32 | else 33 | table.insert(arg_out, arg_in[1]) 34 | end 35 | table.remove(arg_in, 1) 36 | end 37 | return opt_out, arg_out 38 | end 39 | 40 | 41 | -- vi: ft=lua ts=3 sw=3 42 | -------------------------------------------------------------------------------- /app/list.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Generic list class with handy operators 4 | -- 5 | 6 | 7 | local list_meta = { 8 | 9 | __index = { 10 | 11 | -- Add item to end of list 12 | 13 | append = function(list, e) 14 | list[#list+1] = e 15 | return list 16 | end, 17 | 18 | -- Add item to end of list 19 | 20 | push = function(list, e) 21 | list[#list+1] = e 22 | return list 23 | end, 24 | 25 | -- Insert item at front of list 26 | 27 | prepend = function(list, e) 28 | table.insert(list, 1, e) 29 | return list 30 | end, 31 | 32 | unshift = function(list, e) 33 | table.insert(list, 1, e) 34 | return list 35 | end, 36 | 37 | -- Return copy of list 38 | 39 | copy = function(list) 40 | local list2 = List() 41 | for e in list:each() do 42 | list2:append(e) 43 | end 44 | return list2 45 | end, 46 | 47 | -- Return first element 48 | 49 | head = function(list) 50 | return list[1] 51 | end, 52 | 53 | -- Return copy of list omitting first element 54 | 55 | tail = function(list) 56 | return list:splice(2) 57 | end, 58 | 59 | -- transpose list-of-list 60 | 61 | zip = function(list) 62 | local list2 = List() 63 | for i, row in ipairs(list) do 64 | for j, col in ipairs(row) do 65 | list2[j] = list2[j] or List() 66 | list2[j][i] = col 67 | end 68 | end 69 | return list2 70 | end, 71 | 72 | -- fold function 73 | 74 | fold = function(list, fn) 75 | local r 76 | for a in list:each() do 77 | r = r and fn(r, a) or a 78 | end 79 | return r 80 | end, 81 | 82 | -- Remove and return last element 83 | 84 | pop = function(list) 85 | local e = list[1] 86 | table.remove(list) 87 | return e 88 | end, 89 | 90 | -- Remove and return first element 91 | 92 | shift = function(list) 93 | local e = list[1] 94 | table.remove(list, 1) 95 | return e 96 | end, 97 | 98 | -- Take first n elements 99 | 100 | take = function(list, n) 101 | return list:splice(1, n) 102 | end, 103 | 104 | -- Return splice of list in given range 105 | 106 | splice = function(list, a, b) 107 | local o = List() 108 | for i = (a or 1), (b or #list) do 109 | o:append(list[i]) 110 | end 111 | return o 112 | end, 113 | 114 | -- Return a new list with the elements in reverse order 115 | 116 | reverse = function(list) 117 | local list2 = List() 118 | local n = #list 119 | for i = 1, n do 120 | list2[i] = list[n-i+1] 121 | end 122 | return list2 123 | end, 124 | 125 | -- Iterate over each element in the list 126 | 127 | each = function(list) 128 | local n = 0 129 | return function() 130 | n = n + 1 131 | if list[n] then return list[n] end 132 | end 133 | end, 134 | 135 | -- functional map 136 | 137 | map = function(list, fn) 138 | if type(fn) == "string" then fn = lambda(fn) end 139 | local l2 = List() 140 | for e in list:each() do 141 | l2:append(fn(e)) 142 | end 143 | return l2 144 | end, 145 | 146 | -- functional map on 2 tables 147 | 148 | map2 = function(list, list2, fn) 149 | local out = List() 150 | for i = 1, #list do 151 | out:append(fn(list[i], list2[i])) 152 | end 153 | return out 154 | end, 155 | 156 | -- Sort 157 | 158 | sort = function(list, fn) 159 | table.sort(list, fn) 160 | return list 161 | end, 162 | 163 | -- Concatenate all elements into a string 164 | 165 | concat = function(list, sep) 166 | return table.concat(list, sep) 167 | end, 168 | 169 | -- Unpack to argument list 170 | 171 | unpack = function(list) 172 | return unpack(list) 173 | end, 174 | 175 | -- Return number of elements on the list 176 | 177 | size = function(list) 178 | return #list 179 | end, 180 | 181 | -- Return true if empty 182 | 183 | is_empty = function(list) 184 | return #list == 0 185 | end, 186 | 187 | -- Return true if the given item is present in the list 188 | 189 | contains = function(list, e) 190 | for n = 1, #list do 191 | if list[n] == e then return true 192 | end 193 | end 194 | end, 195 | 196 | remove = function(list, e) 197 | for n = 1, #list do 198 | if list[n] == e then 199 | table.remove(list, n) 200 | end 201 | end 202 | 203 | end 204 | 205 | } 206 | } 207 | 208 | 209 | function List(t) 210 | local list = t or {} 211 | setmetatable(list, list_meta) 212 | return list 213 | end 214 | 215 | 216 | 217 | -- vi: ft=lua ts=3 sw=3 218 | 219 | -------------------------------------------------------------------------------- /app/log.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/app/log.lua -------------------------------------------------------------------------------- /app/misc.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Create a '%' operator on strings for ruby- and python-like formatting 4 | -- 5 | 6 | getmetatable("").__mod = function(a, b) 7 | if not b then 8 | return a 9 | elseif type(b) == "table" then 10 | return string.format(a, unpack(b)) 11 | else 12 | return string.format(a, b) 13 | end 14 | end 15 | 16 | 17 | -- 18 | -- Fix up line numbers of loaded chunks in stack traces and error messages 19 | -- 20 | 21 | function fixup_error(msg) 22 | return msg:gsub('%[string "live (.-):(%d+)"]:(%d+)', function(n, l1, l2) 23 | return n .. ":" .. (l1+l2-1) 24 | end) 25 | end 26 | 27 | 28 | -- 29 | -- Safe call: calls the given function with an error handler, print error 30 | -- and stacktrace to stdout 31 | -- 32 | 33 | function safecall(fn, ...) 34 | local function errhandler(err) 35 | local msg = debug.traceback("Error: " .. err, 3) 36 | print(fixup_error(msg)) 37 | end 38 | if type(fn) == "string" then fn = sandbox:get(fn) end 39 | return xpcall(fn, errhandler, ...) 40 | end 41 | 42 | 43 | -- 44 | -- Dump lua data structure 45 | -- 46 | 47 | function dump(obj, maxdepth) 48 | 49 | maxdepth = maxdepth or 999 50 | local dumped = {} 51 | local fd = io.stderr 52 | 53 | local function _dumpf(obj, depth) 54 | 55 | local i, n, v 56 | 57 | if(type(obj) == "table") then 58 | fd:write("{") 59 | if depth < maxdepth then 60 | if not dumped[obj] then 61 | dumped[obj] = true 62 | for n, v in pairs(obj) do 63 | fd:write("\n"); 64 | fd:write(string.rep(" ", depth+1)) 65 | _dumpf(n, depth+1); 66 | fd:write(" = ") 67 | _dumpf(v, depth+1); 68 | fd:write(", ") 69 | end 70 | fd:write("\n") 71 | fd:write(string.rep(" ", depth)) 72 | else 73 | fd:write(" *** ") 74 | end 75 | else 76 | fd:write(" <<< ") 77 | end 78 | fd:write("}") 79 | local mt = getmetatable(obj) 80 | if mt then 81 | fd:write(" metatable: ") 82 | _dumpf(mt, depth) 83 | end 84 | elseif(type(obj) == "string") then 85 | fd:write("%q" % obj) 86 | elseif(type(obj) == "number") then 87 | fd:write(obj) 88 | elseif(type(obj) == "boolean") then 89 | fd:write(obj and "true" or "false") 90 | elseif(type(obj) == "function") then 91 | local i = debug.getinfo(obj) 92 | fd:write("function(%s:%s)" % { i.short_src, i.linedefined }) 93 | else 94 | fd:write("(%s)" % type(obj)) 95 | end 96 | 97 | if depth == 0 then 98 | fd:write("\n") 99 | io.flush() 100 | end 101 | end 102 | 103 | _dumpf(obj, 0) 104 | end 105 | 106 | 107 | -- 108 | -- Serialize lua data to string 109 | -- 110 | 111 | function serialize(o) 112 | local t = type(o) 113 | if t == "string" then 114 | return string.format("%q", o) 115 | elseif t == "boolean" then 116 | return tostring(o) 117 | elseif t == "number" then 118 | return tostring(o) 119 | elseif t == "table" then 120 | local out = {} 121 | local done = {} 122 | for i, c in ipairs(o) do 123 | table.insert(out, serialize(c)) 124 | done[i] = true 125 | end 126 | for k, c in pairs(o) do 127 | if not done[k] then 128 | if type(k) ~= "function" and type(k) ~= "table" then 129 | table.insert(out, "[" .. serialize(k) .. "]=" .. serialize(c)) 130 | end 131 | end 132 | end 133 | return "{" .. table.concat(out, ",") .. "}" 134 | else 135 | return "nil" 136 | end 137 | end 138 | 139 | -- vi: ft=lua ts=3 sw=3 140 | -------------------------------------------------------------------------------- /app/sandbox.lua: -------------------------------------------------------------------------------- 1 | 2 | Sandbox = {} 3 | 4 | local function sandbox_load(sandbox, code, name) 5 | local fn, err = load(code, name, "t", sandbox.env) 6 | if fn then 7 | local co = coroutine.create(fn) 8 | safecall(resumer(co)) 9 | else 10 | print("Error: " .. fixup_error(err)) 11 | end 12 | end 13 | 14 | 15 | local function sandbox_get(sandbox, name) 16 | return sandbox.env[name] 17 | end 18 | 19 | 20 | function Sandbox:new() 21 | 22 | local sandbox = { 23 | 24 | -- methods 25 | 26 | load = sandbox_load, 27 | get = sandbox_get, 28 | 29 | -- data 30 | 31 | srate = 44100, 32 | env = {} 33 | } 34 | 35 | sandbox.env = autoload("sandbox", _G, "lib") 36 | sandbox.env._ENV = sandbox.env 37 | 38 | return sandbox 39 | end 40 | 41 | -- vi: ft=lua ts=3 sw=3 42 | -------------------------------------------------------------------------------- /app/strict.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- strict.lua 3 | -- checks uses of undeclared global variables 4 | -- All global variables must be 'declared' through a regular assignment 5 | -- (even assigning nil will do) in a main chunk before being used 6 | -- anywhere or assigned to inside a function. 7 | -- 8 | 9 | local mt = getmetatable(_G) 10 | if mt == nil then 11 | mt = {} 12 | setmetatable(_G, mt) 13 | end 14 | 15 | mt.__declared = {} 16 | 17 | mt.__newindex = function (t, n, v) 18 | if not mt.__declared[n] then 19 | local w = debug.getinfo(2, "S").what 20 | if w ~= "main" and w ~= "C" then 21 | error("assign to undeclared variable '"..n.."'", 2) 22 | end 23 | mt.__declared[n] = true 24 | end 25 | rawset(t, n, v) 26 | end 27 | 28 | mt.__index = function (t, n) 29 | if not mt.__declared[n] and debug.getinfo(2, "S").what ~= "C" then 30 | error("Accessing undeclared variable '"..n.."'", 2) 31 | end 32 | return rawget(t, n) 33 | end 34 | 35 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | webalizer 3 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | 2 | PAGES := index architecture concepts jack dsp modules examples live_coding gui libs about 3 | 4 | LIBS := $(wildcard ../lib/*.lua ../lib/Dsp/*.lua) 5 | 6 | HTML += $(addsuffix .html, $(PAGES)) 7 | HTML += libs.html 8 | 9 | all: $(HTML) 10 | 11 | %.html: %.txt bin/txt2html 12 | bin/txt2html $(PAGES) < $< > $@ 13 | 14 | libs.txt: bin/libdocs $(LIBS) 15 | bin/libdocs $(LIBS) > $@ 16 | 17 | clean: 18 | rm -f *.html 19 | 20 | -------------------------------------------------------------------------------- /doc/about.txt: -------------------------------------------------------------------------------- 1 | 2 | == Dependencies 3 | 4 | Worp has the following dependencies: 5 | 6 | * LuaJIT >= 2.0.0 7 | * Fluidsynth (optional) 8 | * LGI (optional) 9 | * Linuxsampler (optional) 10 | 11 | 12 | == Misc 13 | 14 | During the development of Worp I learned of the existence of Impromptu and 15 | SuperCollider, which both have features similar to worp (Event scheduling, live 16 | coding, etc). I've shamelessly stolen some ideas here and there from both 17 | projects where I saw fit. 18 | 19 | * [http://impromptu.moso.com.au] 20 | * [http://supercollider.sourceforge.net] 21 | 22 | This would probably be a good place to write about the likes and the 23 | differences between these systems and Worp, but I feel I don't know enough 24 | about either to be able to write anything useful here. 25 | 26 | 27 | == License 28 | 29 | Copyright (C) 2013 Ico Doornekamp. 30 | 31 | This program is free software; you can redistribute it and/or modify it under 32 | the terms of the GNU General Public License version 2 as published by the Free 33 | Software Foundation. 34 | 35 | 36 | -------------------------------------------------------------------------------- /doc/appregiator.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/appregiator.mp3 -------------------------------------------------------------------------------- /doc/architecture.txt: -------------------------------------------------------------------------------- 1 | 2 | = Architecture 3 | 4 | Worp is written mostly in Lua (except for some C code for binding to the OS and 5 | Jack), and is run using LuaJIT. LuaJIT is an alternate implementation of the 6 | Lua language that is highly optimized and can deliver near native performance 7 | for numeric applications, which comes in handy for generating sound and doing 8 | digital signal processing. 9 | 10 | == Running code 11 | 12 | At startup, Worp creates a Lua sandbox and is ready to receive Lua code to 13 | process. Any scripts to run at startup can be passed on the command line, or 14 | chunks of Lua code can be sent over a local TCP socket to the Worp process. The 15 | network connection is used by a Vim plug-in to send blocks or lines of code 16 | directly from the editor to Worp, which will compile and run the code without 17 | disrupting running audio. 18 | 19 | == Loading libraries 20 | 21 | When worp encounters a reference to an undefined variable in the sandbox, it 22 | will check if a library is available which will provide this symbol, and load 23 | it on the fly. 24 | 25 | The current method looks for a file with the given name in the ./lib directory, 26 | load the chunk and use the return value of the chunk. 27 | -------------------------------------------------------------------------------- /doc/bin/libdocs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | table.sort(arg) 4 | 5 | print [[ 6 | 7 | = Libraries 8 | 9 | This page briefly describes all available library functions. 10 | 11 | 12 | ]] 13 | 14 | for _, fname in ipairs(arg) do 15 | 16 | local lib = fname:gsub("../lib/", ""):gsub(".lua", ""):gsub("/", ":") 17 | 18 | print("") 19 | print("== " .. lib) 20 | print("") 21 | 22 | local f = io.open(fname) 23 | if f then 24 | local a = f:read("*a") 25 | f:close() 26 | 27 | -- Get first comment block of file 28 | 29 | doc = a:match("^%s*(%-%-.-)\n\n") 30 | if doc then 31 | doc = ("\n" .. doc):gsub("%-%- ?", "") 32 | print(doc) 33 | end 34 | 35 | -- Cut out 'controls' section of module, assuming only 36 | -- a single module is defined in each file 37 | 38 | cs = a:match("controls%s+=%s+(%b{})") 39 | if cs then 40 | ok, cs = pcall(loadstring("return " .. cs)) 41 | if ok then 42 | print("Controls:") 43 | print("") 44 | for _, c in ipairs(cs) do 45 | local range = "" 46 | if c.options and #c.options > 0 then 47 | range = table.concat(c.options, "/") 48 | else 49 | range = (c.min or 0) .. ".." .. (c.max or 1) 50 | end 51 | print("* " .. c.id .. " : " .. c.description .. " (" .. (c.type or "number") .. ", " .. range .. ")") 52 | end 53 | end 54 | end 55 | end 56 | end 57 | 58 | 59 | -- vi: ft=lua ts=3 sw=3 60 | -------------------------------------------------------------------------------- /doc/bin/txt2html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local menu = {} 4 | for i, page in ipairs(arg) do 5 | menu[i] = "" .. page:gsub("_", " ") .. "" 6 | end 7 | menu = table.concat(menu, " | ") 8 | 9 | 10 | function code(s) 11 | local fname = os.tmpname() 12 | local fd = io.open(fname, "w") 13 | fd:write(s) 14 | fd:close() 15 | 16 | local h = io.popen("highlight --fragment --css=/dev/null --syntax lua " .. fname, "r") 17 | local v = h:read("*a") 18 | h:close() 19 | 20 | return "
" .. v .. "
\n\n" 21 | end 22 | 23 | function link(s) 24 | if s:find("png$") then 25 | return "" 26 | elseif s:find("mp3$") then 27 | return [[ 28 |
29 | ]] .. s .. [[
30 | 31 | 32 |
33 | ]] 34 | elseif s:find("vimeo") then 35 | local id = s:match("%d+") 36 | return [[
]] 38 | else 39 | local u = s 40 | if not u:find("^http") then u = u .. ".html" end 41 | return "" .. s .. "" 42 | end 43 | end 44 | 45 | function list(s, n) 46 | return "\n\n" .. n 47 | end 48 | 49 | local toc = {} 50 | 51 | function header(s) 52 | table.insert(toc, s) 53 | end 54 | 55 | local c = io.read("*a") 56 | 57 | c = c:gsub("<", "<") 58 | c = c:gsub(">", ">") 59 | c = c:gsub("'(%w+)'", "%1") 60 | c = c:gsub("_(%w+)_", "%1") 61 | c = c:gsub("\n=+ ([^\n]+)", header) 62 | c = c:gsub("\n= ([^\n]+)", "

%1

") 63 | c = c:gsub("\n== ([^\n]+)", "

%1

") 64 | c = c:gsub("\n(\n .-)\n\n", code) 65 | c = c:gsub("%[([%S+]+)%]", link) 66 | c = c:gsub("NOTE: (.-)\n\n", "
%1
\n\n") 67 | c = c:gsub("(\n%*.-)\n([^%s%*])", list) 68 | c = c:gsub("\n\n", "\n\n

\n") 69 | 70 | for i = 1, #toc do 71 | local s = toc[i] 72 | toc[i] = "" .. s .. "" 73 | end 74 | toc = table.concat(toc, "
") 75 | 76 | print([[ 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |

89 | 90 | 91 | 101 | 102 |
92 |
93 |

94 | ]] .. toc .. [[ 95 |

96 |
97 |
98 | ]] .. c .. [[ 99 |
100 |
103 | 104 | 105 | ]]) 106 | 107 | 108 | -- vi: ft=lua ts=3 sw=3 109 | -------------------------------------------------------------------------------- /doc/concepts.txt: -------------------------------------------------------------------------------- 1 | 2 | = Basic concepts 3 | 4 | Worp is built on the following core concepts: 5 | 6 | == The sandbox 7 | 8 | Worp scripts are run in a Lua sandbox. This is simple a separate namespace in 9 | the Lua interpreter where code can be loaded without interfering with Worp 10 | itself. 11 | 12 | Any variables and functions which are not defined local will end up being 13 | globals in the sandbox namespace. While globals are usually considered bad 14 | practice, they come in handy when using live programming: when new chunks are 15 | sent from the editor to Worp they are run in the same sandbox and can access or 16 | alter any defined variables. 17 | 18 | == Instruments 19 | 20 | An instrument in Worp is basically a function which, when called, will start 21 | generating sound at the given note and given volume. Calling the same function 22 | more than once will play more then one note for polyphonic instruments, and 23 | calling the same function for an active note with the volume set to zero will 24 | mute the note. 25 | 26 | Worp uses midi note numbers for instruments, where 60 is defined as the middle 27 | C at 261.6 Hz. Volume is given in the range 0 to 1. 28 | 29 | For example, the following code will play the middle C on piano for 1 second at 30 | medium volume (assuming the function 'piano' has been defined earlier): 31 | 32 | piano(60, 0.8) at(1, function() 33 | piano(60, 0) 34 | end) 35 | 36 | Because Worp is all about playing notes, the 'play()' function is provided as a 37 | shorthand. Again playing the middle C for one second: 38 | 39 | play(piano, 0.8, 1) 40 | 41 | 42 | == Time 43 | 44 | Time in Worp is handled by the event scheduler, which does nothing more than 45 | calling functions at a requested time. To schedule a function for future 46 | execution, use the built in function 'at': 47 | at(TIME, FUNCTION [, ...]) 48 | 49 | For example, the following line will print the text "Hello world" after one 50 | second: 51 | 52 | at(1, function() 53 | print("Hello", "world") 54 | end) 55 | 56 | The 'at()' function has a shorthand notation for functions which are defined 57 | globally: 58 | 59 | at(1, "print", "Hello", "world") 60 | 61 | Worp takes care not to let time slip unexpectedly. Consider the following 62 | fragment: 63 | 64 | at(1, 'play', piano, 60, 0.3) 65 | at(1, 'play', piano, 72, 0.3) 66 | 67 | If at() would use the current time as reference for scheduling function calls 68 | in the future, there would be no guarantee that the two notes would be played 69 | at the same time: if any time goes by between the two calls to 'at()', the two 70 | calls will be scheduled to run at a different time. 71 | 72 | Instead, at() uses another time reference: the time the current function was 73 | scheduled to run, even if for some reason the current function was scheduled 74 | too early or too late. 75 | 76 | The result is that the following code: 77 | 78 | function tick() 79 | print("tick") 80 | at(1, tick) 81 | end 82 | tick() 83 | 84 | is sure to call the tick() function at exact 1 second intervals on average, 85 | without introducing additional delay caused by the time needed to run the code. 86 | 87 | On top of this function more complex objects such as metronomes and pattern 88 | generators can be built. 89 | 90 | -------------------------------------------------------------------------------- /doc/dsp.txt: -------------------------------------------------------------------------------- 1 | 2 | = DSP 3 | 4 | Worp comes with a library with basic constructs for doing audio synthesis and 5 | processing. This library provides abstracts for 'modules', which are objects 6 | that process of generate sound, and can be controlled by 'controls'. 7 | 8 | == Modules 9 | 10 | Technicaly, a module is a Lua table with a __call entry in its metatable. This 11 | allows the table to be called like a function, but also to have callable methods. 12 | 13 | Modules are instantiated by calling the appropriate library function, and can 14 | optionally be passed initial values for the controls. The following will generate 15 | a sine oscillator with an initial frequency of 500 Hz: 16 | 17 | osc = Dsp:Osc { f = 1500 } 18 | 19 | osc can be handled as a function, and will generate a new sample each time it is called. 20 | 21 | For example, to see the above oscillator in action try: 22 | 23 | for i = 1, 100 do 24 | print(osc()) 25 | end 26 | 27 | The Worp console output will print the sample values: 28 | 29 | 0.21209065141554 30 | 0.41453117669030 31 | 0.59811053049122 32 | ... 33 | 34 | Some modules like filters can also process audio, and will take 35 | one or more sample values when called. To send the output of a 30 Hz saw wave 36 | into a 100 Hz low pass filter with a resonance of 3: 37 | 38 | o = Dsp:Saw { f = 30 } 39 | f = Dsp:Filter { f = 100, Q = 3 } 40 | 41 | for = i 1, 100 do 42 | print(f(o()) 43 | end 44 | 45 | For more details, check the [modules] page 46 | 47 | 48 | == Controls 49 | 50 | Modules often have one or more controls to effect the behaviour. The module 51 | object provides a handy :help() method to show what controls are available: 52 | 53 | f = Dsp:Filter { f = 100, Q = 3 } 54 | f:help() 55 | 56 | will print 57 | 58 | Biquad filter: 59 | - type: Filter type (lp/hp/bp/bs/ls/hs/eq/ap) 60 | - f: Frequency (0..20000) 61 | - Q: Resonance (0.1..100) 62 | - gain: Shelf/EQ filter gain (-60..60) 63 | 64 | To set the value of a control, use the :set() method: 65 | 66 | f:set { type = "hp", f = 200 } 67 | 68 | 69 | The Jack library knows about controls, and provides a shorthand to map controls 70 | to midi CC's. The following snippet will map all four filter controls to CC 10, 71 | 11, 12 and 13 on midi channel 1: 72 | 73 | j = Jack:new("worp") 74 | f = Dsp:Filter() 75 | m = Jack:midi("midi") 76 | m:map_mod(1, 10, f) 77 | 78 | To map a single control, use: 79 | 80 | m:map_control(1, 10, f:control "Q") 81 | 82 | 83 | == Generating sound 84 | 85 | This is where the Jack and Dsp libraries come together. The fragment 86 | below will low-pass filter a saw wave, send the result to a jack audio 87 | port called 'worp:synth-out-1', and connect this port to all available 88 | input ports in the system: 89 | 90 | s = Dsp:Saw { f = 50 } 91 | f = Dsp:Filter { ft = "lp", f = 1500, Q = 4 } 92 | 93 | j = Jack:new("worp") 94 | 95 | j:dsp("synth", 0, 1, function() 96 | return f(s()) 97 | end) 98 | 99 | j:connect("worp") 100 | 101 | 102 | -------------------------------------------------------------------------------- /doc/example.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/example.mp3 -------------------------------------------------------------------------------- /doc/examples.txt: -------------------------------------------------------------------------------- 1 | 2 | = Examples 3 | 4 | This page is a bit too empty at this time. 5 | 6 | More examples are available in the /examples directory. 7 | 8 | 9 | == Polyphonic synth appregiator 10 | 11 | A polyphonic synthesizer with filtering envelope, played by a simple appregiator. 12 | 13 | 14 | [appregiator.mp3] 15 | 16 | 17 | j = Jack:new() 18 | 19 | function Synth() 20 | 21 | local o1 = Dsp:Square() 22 | local o2 = Dsp:Saw() 23 | local f = Dsp:Filter { f = 800, Q = 8 } 24 | local adsr = Dsp:Adsr { A = 0.01, D = 0.01, S = 0.3 } 25 | local adsr2 = Dsp:Adsr { A = 0.03, D = 1.00, S = 0.1 } 26 | local pan = Dsp:Pan() 27 | 28 | local depth = 0 + 0.1 29 | 30 | local function instr(note, vel) 31 | if vel > 0 then depth = vel end 32 | local freq = n2f(note) 33 | o1:set { f = freq } 34 | o2:set { f = freq * 4.0 + 0.2 } 35 | adsr:set { vel = vel } 36 | adsr2:set { vel = vel } 37 | pan:set { pan = rr(-0.8, 0.8) } 38 | end 39 | 40 | local function gen() 41 | f:set { f = adsr2() * 3000 * depth + 20} 42 | return pan(0.1 * adsr() * f(o1() + o2() + math.random() * 0.75)) 43 | end 44 | 45 | return instr, gen 46 | end 47 | 48 | instr, gen = Poly(Synth) 49 | 50 | rev = Dsp:Reverb { damp = 0.2 } 51 | 52 | j:dsp("synth", 0, 2, function() 53 | return rev(gen()) 54 | end) 55 | 56 | j:connect("worp") 57 | 58 | function play2(instr, note, vel, dur) 59 | play(instr, note, vel, dur) 60 | at(0.18*3, play, instr, note + 12, vel * 0.9, dur) 61 | end 62 | 63 | ns = { 34, 22, 70, 34, 65, 34, 17, 74, 36, 72, 53, 58 } 64 | 65 | function appreg(i) 66 | 67 | local n = ns[i] 68 | local v = (i % 5) == 1 and 0.9 or 0.7 69 | play2(instr, n, v, 0.16) 70 | 71 | at(0.18, function() 72 | appreg((i % #ns) + 1) 73 | end) 74 | end 75 | 76 | appreg(1) 77 | 78 | 79 | == Linuxsampler + DSP effects 80 | 81 | The fragment below plays a piano loop with Linuxsampler, adds a wah-wah effect 82 | with a LFO-controlled bandpass filter, and plays some simple reverberated 83 | percussion in the background: 84 | 85 | [example.mp3] 86 | 87 | 88 | j = Jack:new("worp") 89 | l = Linuxsampler:new("piano", "/opt/samples") 90 | m = Metro:new(150, 10) 91 | v = l:add("piano/megapiano.gig", 0) 92 | n = Dsp:Noise() 93 | nf = Dsp:Filter { type = "bp", f = 8000, Q = 5 } 94 | p = Dsp:Pan() 95 | a = Dsp:Adsr { A = 0, D = 0.03, S = 1, R = 0.05 } 96 | r = Dsp:Reverb { } 97 | f = Dsp:Filter { type = "bp", Q = 5 } 98 | lfo = Dsp:Osc { f = 4 / m:t_meas() } 99 | 100 | ns = { 36, 75, 79, 84, 34, 75, 79, 74, 84, 82 } 101 | 102 | function loop(i) 103 | play(v, ns[i], ns[i] < 40 and 0.8 or 0.6, 1.0) 104 | i = (i % #ns) + 1 105 | at(m:t_beat(), loop, i) 106 | end 107 | 108 | j:dsp("wah", 1, 1, function(vi) 109 | f:set { f = lfo() * 500 + 700 } 110 | return f(vi) 111 | end) 112 | 113 | j:dsp("perc", 0, 2, function() 114 | return p( r( nf( a() * n() ) ) ) 115 | end) 116 | 117 | function click() 118 | nf:set { f = rr(8000, 12000) } 119 | p:set { pan = rr(-1, 1) } 120 | a:set { vel = rr(0.2, 0.8) } 121 | at(0.01, function() a:set { vel = 0 } end) 122 | at(m:t_beat() * 0.5, "click") 123 | end 124 | 125 | loop(1) 126 | click() 127 | 128 | j:connect("piano", "worp:wah") 129 | j:connect("worp:wah", "system:playback") 130 | j:connect("worp:perc", "system:playback") 131 | 132 | 133 | == Linuxsampler 134 | 135 | Create a linuxsampler instrument, and map this to midi channel 1. 136 | 137 | ls = Linuxsampler:new("synth", "/opt/samples") 138 | j = Jack:new("worp") 139 | midi = j:midi() 140 | piano = ls:add("piano/Bosendorfer.gig", 0) 141 | midi:map_instr(1, piano) 142 | j:connect("synth") 143 | j:connect("worp") 144 | 145 | -------------------------------------------------------------------------------- /doc/gui.txt: -------------------------------------------------------------------------------- 1 | 2 | = GUI 3 | 4 | Support for a graphical user interface is under development. The Gui library 5 | creates a window where controls and modules can be added. 6 | 7 | == Example 8 | 9 | For example, the following fragment: 10 | 11 | gui = Gui:new() 12 | f = Dsp:Filter() 13 | gui:add_mod(f) 14 | 15 | will generate this user interface window, mapping each control of the module to 16 | a suitable graphical representation: 17 | 18 | [gui.png] 19 | 20 | Turning the knobs will adjust the control values, and any changes in the controls 21 | will reflect in the knob positions. 22 | 23 | NOTE: The current code does not handle fast control updates well, so use with care. 24 | 25 | 26 | == Implementation 27 | 28 | The Gui is generated using Gtk through the LGI library: 29 | [https://github.com/pavouk/lgi] 30 | 31 | -------------------------------------------------------------------------------- /doc/htdocs/highlight.css: -------------------------------------------------------------------------------- 1 | /* Style definition file generated by highlight 3.8, http://www.andre-simon.de/ */ 2 | 3 | /* Highlighting theme: Kwrite Editor */ 4 | 5 | pre.hl { margin-left: 40px; color:#000000; font-size:11pt; font-family:'Courier New';} 6 | .hl.num { color:#b07e00; } 7 | .hl.esc { color:#ff00ff; } 8 | .hl.str { color:#bf0303; } 9 | .hl.pps { color:#818100; } 10 | .hl.slc { color:#838183; font-style:italic; } 11 | .hl.com { color:#838183; font-style:italic; } 12 | .hl.ppc { color:#008200; } 13 | .hl.opt { color:#000000; } 14 | .hl.lin { color:#555555; } 15 | .hl.kwa { color:#000000; font-weight:bold; } 16 | .hl.kwb { color:#0057ae; } 17 | .hl.kwc { color:#000000; font-weight:bold; } 18 | .hl.kwd { color:#010181; } 19 | 20 | -------------------------------------------------------------------------------- /doc/htdocs/style.css: -------------------------------------------------------------------------------- 1 | 2 | html { 3 | margin-left: 0px; 4 | } 5 | 6 | body { 7 | margin-left: 0px; 8 | margin-top: 20px; 9 | } 10 | 11 | div, p, h1, h2, li, a, .warn { 12 | font-family: 'Text Me One'; 13 | } 14 | 15 | 16 | #menu { 17 | background-image: url(../img/worp-long.png); 18 | background-repeat: no-repeat; 19 | height: 60px; 20 | padding-top: 60px; 21 | text-align: right; 22 | } 23 | 24 | img { 25 | border: solid 1px black; 26 | } 27 | 28 | .video { 29 | text-align: center; 30 | } 31 | 32 | .mp3 { 33 | margin-left: 100px; 34 | width: 400px; 35 | text-align: center; 36 | } 37 | 38 | #tocmain { 39 | margin-top: 20px; 40 | margin-left: auto; 41 | margin-right: auto; 42 | width: 80%; 43 | } 44 | 45 | #toc { 46 | white-space: nowrap; 47 | text-align: right; 48 | margin-right: 20px; 49 | } 50 | 51 | #main { 52 | } 53 | 54 | td { 55 | vertical-align: top; 56 | } 57 | 58 | p { 59 | margin-left: 20px; 60 | } 61 | 62 | a { 63 | color: #ff3300; 64 | text-decoration: none; 65 | } 66 | 67 | h1 { 68 | font-size: 1.2em; 69 | border-bottom: solid 1px #cccccc; 70 | } 71 | 72 | h2 { 73 | font-size: 1em; 74 | } 75 | 76 | 77 | .warn { 78 | margin-left: 20px; 79 | color: #880000; 80 | border-left: solid 3px red; 81 | padding-left: 5px; 82 | } 83 | 84 | // vi: ts=3 sw=3 85 | 86 | -------------------------------------------------------------------------------- /doc/htdocs/zplayer.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/htdocs/zplayer.swf -------------------------------------------------------------------------------- /doc/img/connect-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/img/connect-1.png -------------------------------------------------------------------------------- /doc/img/connect-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/img/connect-2.png -------------------------------------------------------------------------------- /doc/img/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/img/gui.png -------------------------------------------------------------------------------- /doc/img/worp-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/img/worp-icon.png -------------------------------------------------------------------------------- /doc/img/worp-long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/doc/img/worp-long.png -------------------------------------------------------------------------------- /doc/index.txt: -------------------------------------------------------------------------------- 1 | 2 | = Introduction 3 | 4 | Worp is an experimental sound / music / DSP engine written in LuaJIT, currently 5 | in early alpha state. 6 | 7 | == Example code 8 | 9 | The fragment below plays a piano loop with Linuxsampler, adds a wah-wah effect 10 | with a LFO-controlled bandpass filter, and plays some simple reverberated 11 | percussion in the background: 12 | 13 | [example.mp3] 14 | 15 | 16 | j = Jack:new("worp") 17 | l = Linuxsampler:new("piano", "/opt/samples") 18 | m = Metro:new(150, 10) 19 | v = l:add("piano/megapiano.gig", 0) 20 | n = Dsp:Noise() 21 | nf = Dsp:Filter { type = "bp", f = 8000, Q = 5 } 22 | p = Dsp:Pan() 23 | a = Dsp:Adsr { A = 0, D = 0.03, S = 1, R = 0.05 } 24 | r = Dsp:Reverb { } 25 | f = Dsp:Filter { type = "bp", Q = 5 } 26 | lfo = Dsp:Osc { f = 4 / m:t_meas() } 27 | 28 | ns = { 36, 75, 79, 84, 34, 75, 79, 74, 84, 82 } 29 | 30 | function loop(i) 31 | play(v, ns[i], ns[i] < 40 and 0.8 or 0.6, 1.0) 32 | i = (i % #ns) + 1 33 | at(m:t_beat(), loop, i) 34 | end 35 | 36 | j:dsp("wah", 1, 1, function(vi) 37 | f:set { f = lfo() * 500 + 700 } 38 | return f(vi) 39 | end) 40 | 41 | j:dsp("perc", 0, 2, function() 42 | return p( r( nf( a() * n() ) ) ) 43 | end) 44 | 45 | function click() 46 | nf:set { f = rr(8000, 12000) } 47 | p:set { pan = rr(-1, 1) } 48 | a:set { vel = rr(0.2, 0.8) } 49 | at(0.01, function() a:set { vel = 0 } end) 50 | at(m:t_beat() * 0.5, "click") 51 | end 52 | 53 | loop(1) 54 | click() 55 | 56 | j:connect("piano", "worp:wah") 57 | j:connect("worp:wah", "system:playback") 58 | j:connect("worp:perc", "system:playback") 59 | 60 | 61 | == Built on LuaJIT 62 | 63 | Worp is built on LuaJIT, a Just-In-Time Compiler (JIT) for the Lua programming 64 | language. LuaJIT offers the flexibility of the Lua programming language, while 65 | delivering near native performance. This allows for low-level DSP code 66 | (filters, reverbs, etc.) to be written in a high level language. 67 | 68 | 69 | == Libraries 70 | 71 | Worp comes with default bindings for Jack audio and midi, and has built-in 72 | support for Linuxsampler and Fluidsynth. A growing library of DSP primitives is 73 | provided: oscillators, filters, delays, pitch-shifters, etc. 74 | 75 | 76 | == Live coding 77 | 78 | Worp supports live coding through the use of a plug-in for your favourite 79 | editor (if your favourite editor is Vim). Code is sent from the editor into 80 | the Worp process, where it is compiled and executed without disrupting the 81 | current program flow. 82 | 83 | Worp is in early development and subject to lots of changes. Expect 84 | documentation to be outdated, examples to fail and API's to be different then 85 | described. 86 | 87 | 88 | = Running 89 | 90 | The Worp source code is hosted on github. Currently there are no official 91 | releases available, so get the latest code from Github and hope for the best. 92 | There is no 'make install' target, simply start Worp from the source directory. 93 | 94 | Check [http://github.com/zevv/worp] for the latest code. 95 | 96 | 97 | $ git clone https://github.com/zevv/worp.git 98 | $ cd worp 99 | $ make 100 | $ ./worp 101 | 102 | -------------------------------------------------------------------------------- /doc/jack.txt: -------------------------------------------------------------------------------- 1 | 2 | = Jack 3 | 4 | While Worp itself does not depend on Jack, a Jack library is available which 5 | provides some essential functionalty for making sound and music. 6 | 7 | To use jack from a worp script, first a jack client object must be created. The 8 | following code creates a jack object 'j', and uses 'foo' as the jack client 9 | name: 10 | 11 | 12 | j = Jack("worp") 13 | 14 | 15 | Now that a jack client is available, we can use it to do some work: 16 | 17 | == Ports 18 | 19 | Connecting Jack ports is done with the ':connect()' method of the jack client 20 | object: 21 | 22 | 23 | j:connect(FROM [,TO]) 24 | 25 | 26 | The connect function does only one thing: it will try to connect all ports 27 | matched by 'FROM' to all ports of the same types matched by 'TO'. If the 'TO' 28 | argument is not given, it will match any jack port in the sytem with the same 29 | port type. 30 | 31 | For example, given a system with the following (unconnected) jack clients and 32 | ports: 33 | 34 | [connect-1.png] 35 | 36 | When calling: 37 | 38 | j:connect("worp") 39 | 40 | Worp will: 41 | 42 | * find all ports starting with the FROM string 'worp'. 43 | 44 | * find all ports starting with the TO string. Since this string is 45 | not given, it will match all Jack ports. 46 | 47 | * For each of the found FROM ports, it will try to find and 48 | connect to any TO port of the same type, but with the opposite direction 49 | (input/output) 50 | 51 | * If the number of matching ports do not match, it will try to do the 52 | right thing to connect a single mono to two stereo ports. 53 | 54 | The result will be the following graph: 55 | 56 | [connect-2.png] 57 | 58 | For more fine grained control, ports can be specified with more detail. Some 59 | examples: 60 | 61 | 62 | j:connect("worp", "system:playback") 63 | j:connect("worp:synth", "system") 64 | j:connect("worp:synth-in") 65 | j:connect("worp:synth-in-1", "system:capture_1") 66 | 67 | 68 | Check the sample scripts for more examples of the usage of the connect() 69 | function. 70 | 71 | 72 | == MIDI 73 | 74 | The jack client provides two methods for receiving midi data: 75 | 76 | 77 | j:on_note(CHANNEL, FN) 78 | j:on_cc(CHANNEL, CC, FN) 79 | 80 | 81 | For example, the following snippet will print all received midi notes and 82 | control changes for midi channel 1 and CC #5: 83 | 84 | 85 | j:on_note(1, function(note, vel) 86 | print("key", note, vel) 87 | end) 88 | 89 | j:on_cc(1, 5, function(val) 90 | print("cc", val) 91 | end) 92 | 93 | 94 | == Sound 95 | 96 | NOTE: The DSP API is currently being reworked, so the documentation below will be 97 | outdated soon 98 | 99 | The jack client object can be used to connect Worp to a number of Jack audio 100 | ports. The call 101 | 102 | 103 | j:dsp(NAME, INPUT_COUNT, OUTPUT_COUNT, FN) 104 | 105 | 106 | will create a number of audio ports with the name NAME-[in|out]-#, and will 107 | from that moment call the given function FN for each audio sample. 108 | 109 | For example, the following function will generate a 1 Khz sine wave on a single 110 | Jack output port called worp:beep-out-1: 111 | 112 | 113 | i, di = 0, 1000 * math.pi * 2 / srate 114 | jack:dsp("beep", 0, 1, function() 115 | i = i + di 116 | return cos(i) 117 | end) 118 | 119 | 120 | Worp provides a convenient abstraction layer for doing digital signal 121 | processing, for more details see the dsp page. 122 | 123 | -------------------------------------------------------------------------------- /doc/libs.txt: -------------------------------------------------------------------------------- 1 | 2 | = Libraries 3 | 4 | This page briefly describes all available library functions. 5 | 6 | 7 | 8 | 9 | == Chord 10 | 11 | 12 | == Core 13 | 14 | 15 | 16 | The Core library defines some handy low level functions: 17 | 18 | rl(LIST): return a random item from the given list 19 | rr(MIN, MAX): return a random number from the given range 20 | ntf(NOTE): return frequency of midi note number NOTE 21 | 22 | 23 | == Dsp:Adsr 24 | 25 | 26 | 27 | Attack / Decay / Sustain / Release module. 28 | 29 | This module generates an envelope amplitude between 0.0 and 1.0. When the 30 | 'vel' argument is set to >0 the envelope generator will start (note on), 31 | when 'vel' is set to zero, the generator will go to the decay phase and fall 32 | down to zero amplitude (note off) 33 | 34 | Controls: 35 | 36 | * vel : Velocity (number, 0..1) 37 | * A : Attack (number, 0..10) 38 | * D : Decay (number, 0..10) 39 | * S : Sustain (number, 0..1) 40 | * R : Release (number, 0..10) 41 | 42 | == Dsp:Const 43 | 44 | 45 | 46 | Generator which outputs a constant value in the range 0..1, controlled by 47 | the 'c' control. Useful for easy mapping of a GUI knob or midi CC to a value. 48 | 49 | Controls: 50 | 51 | * c : Value (number, 0..1) 52 | 53 | == Dsp:Control 54 | 55 | 56 | == Dsp:Filter 57 | 58 | 59 | 60 | The Filter module is a basic audio filter with configurable frequency, resonance and gain. 61 | A number of different filter types are provided: 62 | 63 | * hp: High pass 64 | * lp: Low pass 65 | * bp: Band pass 66 | * bs: Band stop (aka, Notch) 67 | * ls: Low shelf 68 | * hs: High shelf 69 | * ap: All pass 70 | * eq: Peaking EQ filter 71 | 72 | The code is based on a document from Robert Bristow-Johnson, check the original 73 | at [http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt] for more details 74 | about the filter implementation 75 | 76 | Controls: 77 | 78 | * type : Filter type (enum, lp/hp/bp/bs/ls/hs/eq/ap) 79 | * f : Frequency (number, 0..20000) 80 | * Q : Resonance (number, 0.1..100) 81 | * gain : Shelf/EQ filter gain (number, -60..60) 82 | 83 | == Dsp:Mod 84 | 85 | 86 | == Dsp:Noise 87 | 88 | 89 | 90 | Random noise generator module, generates noise in the range -1.0 .. +1.0 91 | 92 | The noise module can generate both uniform and gaussian white noise. 93 | 94 | The gaussian noise is based on code from ..., check the original document at 95 | [http://www.taygeta.com/random/gaussian.html] for more details about the 96 | implementation 97 | 98 | Controls: 99 | 100 | * type : Noise type (enum, uniform/gaussian) 101 | 102 | == Dsp:Osc 103 | 104 | 105 | 106 | The Osc module generates a cosine wave at the given frequency. The output 107 | range is -1.0 .. +1.0 108 | 109 | Controls: 110 | 111 | * f : Frequency (number, 0..20000) 112 | 113 | == Dsp:Pan 114 | 115 | 116 | 117 | Stereo pan. Takes one or two inputs and pans them between the two outputs. 118 | 119 | Controls: 120 | 121 | * pan : Pan (number, -1..1) 122 | 123 | == Dsp:Pitchshift 124 | 125 | Controls: 126 | 127 | * f : Factor (number, 0.5..2) 128 | 129 | == Dsp:Reverb 130 | 131 | 132 | 133 | Freeverb reverberator, based on Jezar's public domain C++ sources. 134 | 135 | This is a relatively simple and cheap stereo reverberator, based on a 136 | cascade of comb and allpass filters. 137 | 138 | Controls: 139 | 140 | * wet : Wet volume (number, 0..1) 141 | * dry : Dry volume (number, 0..1) 142 | * room : Room size (number, 0..1.1) 143 | * damp : Damping (number, 0..1) 144 | 145 | == Dsp:Saw 146 | 147 | 148 | 149 | The Saw module generates a sawtooth wave at the given frequency. The output 150 | range is -1.0 .. +1.0 151 | 152 | Controls: 153 | 154 | * f : Frequency (number, 0..20000) 155 | 156 | == Dsp:Square 157 | 158 | 159 | 160 | The Square module generates a square wave at the given frequency and pwm 161 | offset. The output range is -1.0 .. +1.0 162 | 163 | Controls: 164 | 165 | * f : Frequency (number, 0..20000) 166 | * pwm : PWM (number, 0..1) 167 | 168 | == Dsp:Width 169 | 170 | Controls: 171 | 172 | * width : Stereo width (number, 0..2) 173 | 174 | == Fluidsynth 175 | 176 | 177 | == Gui 178 | 179 | 180 | 181 | See the [gui] documentation page 182 | 183 | 184 | == Jack 185 | 186 | 187 | 188 | See the [jack] documentation page 189 | 190 | 191 | == Linuxsampler 192 | 193 | 194 | 195 | The Linuxsampler library provides a simple method to connect to a running 196 | Linuxsample instance. Example: 197 | 198 | l = Linuxsampler:new("synth", "/opt/samples") 199 | piano = l:add("piano/Bosendorfer.gig", 0) 200 | play(piano, 60, 1) 201 | 202 | Create a new linuxsampler connection. The given JACK_NAME is used for 203 | it's audio ports, the SAMPLE_PATH is an optional directory prefix 204 | for sample files: 205 | 206 | l = Linuxsampler:new(JACK_NAME, SAMPLE_PATH) 207 | 208 | Create in instrument function, using the given sample and linuxsampler instrument ID: 209 | 210 | piano = l:add(SAMPLE_FNAME, INSTRUMENT_NUMBER) 211 | 212 | 213 | == Metro 214 | 215 | 216 | == Mixer 217 | 218 | 219 | == Poly 220 | 221 | 222 | 223 | The Poly function provides an easy way to create polyphonic instruments. 224 | 225 | -------------------------------------------------------------------------------- /doc/live_coding.txt: -------------------------------------------------------------------------------- 1 | 2 | = Live coding 3 | 4 | Worp has been designed to allow live editing of the code without interrupting 5 | sound playback. New chunks of Lua code can be sent to worp trough a local 6 | network socket, and are compiled and executed on the fly. A proof-of-concept 7 | vim plugin is available which can send parts of the current file to a running 8 | worp process. 9 | 10 | Load the plugin with the vim command 11 | 12 | :luafile ~/sandbox/prjs/worp/vimplugin.lua 13 | 14 | The plugin currently remaps the following vim key sequences: 15 | 16 | * ,a : send the whole file to worp 17 | * ,f : send the current function 18 | * ,p : send the current paragraph 19 | * , : send the current line 20 | 21 | 22 | Be careful with locals when using live coding: any functions or variables 23 | defined local during chunk loading will not be available when loading the next 24 | chunk. All code is loaded into the same environment, so use globals instead. 25 | 26 | 27 | test 28 | 29 | -------------------------------------------------------------------------------- /doc/main.txt: -------------------------------------------------------------------------------- 1 | 2 | = About 3 | 4 | Worp is an experimental sound / music / DSP engine written in LuaJIT, currently 5 | in early alpha state. 6 | 7 | Some highlights: 8 | 9 | == Bindings for Jack, Linuxsampler, Fluidsynth: 10 | 11 | Worp comes with default bindings for Jack audio and midi, and has built-in 12 | support for Linuxsampler and Fluidsynth. 13 | 14 | == Built on LuaJIT: 15 | 16 | Worp is built on LuaJIT, a Just-In-Time Compiler (JIT) for the Lua 17 | programming language. LuaJIT gives the flexibility and robustness of a 18 | proven language, while delivering near native performance. This allows for 19 | low-level DSP code (filters, reverbs, etc.) to be written in a high level 20 | language. 21 | 22 | == Live coding: 23 | 24 | Worp supports live coding through the use of a plug-in for your favourite 25 | editor (if your favourite editor is Vim). Code is sent from the editor into 26 | the Worp process, where it is compiled and executed without disrupting the 27 | current program flow. 28 | 29 | Worp is in early development and subject to lots of changes. Expect 30 | documentation to be outdated, examples to fail and API's to be different then 31 | described. 32 | -------------------------------------------------------------------------------- /doc/modules.txt: -------------------------------------------------------------------------------- 1 | 2 | = Modules 3 | 4 | The Dsp library uses 'modules' as an abstraction type for generators and signal 5 | processors. A module is a Lua table with a __call metamethod so it can be 6 | called like a regular function. 7 | 8 | == Making 9 | 10 | Modules are created using the Dsp:Mod() constructor. For example, the Pan module 11 | looks like this: 12 | 13 | 14 | function Dsp:Pan(init) 15 | 16 | local v1, v2 17 | 18 | return Dsp:Mod({ 19 | description = "Pan", 20 | controls = { 21 | { 22 | id = "pan", 23 | description = "Pan", 24 | min = -1, 25 | max = 1, 26 | default = 0, 27 | fn_set = function(val) 28 | v1 = math.min(1 + val, 1) 29 | v2 = math.min(1 - val, 1) 30 | end, 31 | }, 32 | }, 33 | 34 | fn_gen = function(i1, i2) 35 | i2 = i2 or i1 36 | return i1*v1, i2*v2 37 | end 38 | 39 | }, init) 40 | 41 | end 42 | 43 | 44 | == Using 45 | 46 | To create an instance of the above module, call the constructor: 47 | 48 | pan = Dsp:Pan() 49 | 50 | This wil generate a Pan module with default values. It is possible 51 | to initialize controls at construction time: 52 | 53 | pan = Dsp:Pan { pan = -0.5 } 54 | 55 | Controls can be adjusted by using the 'set' method. 56 | 57 | pan:set { "pan" = 1 } 58 | 59 | To run the module, simply call the pan() object with the input samples 60 | as arguments: 61 | 62 | o1, o2 = pan(i1, i2) 63 | 64 | 65 | == Details 66 | 67 | What's happening here: 68 | 69 | 70 | function Dsp:Pan(init) 71 | 72 | local v1, v2 73 | 74 | Here the function is defined, with some local variables which will be available to the code 75 | below as upvalues 76 | 77 | return Dsp:Mod({ 78 | description = "Pan", 79 | 80 | Calling the constructor, which takes a table with the modules definition. 81 | 82 | controls = { 83 | { 84 | id = "pan", 85 | description = "Pan", 86 | min = -1, 87 | max = 1, 88 | default = 0, 89 | fn_set = function(val) 90 | v1 = math.min(1 + val, 1) 91 | v2 = math.min(1 - val, 1) 92 | end, 93 | }, 94 | }, 95 | 96 | These are the modules controls. The Pan module has only one which is called 'pan'. This control 97 | ranges from -1 to 1, and has a default of 0. When changed, the fn_set() function is called. 98 | 99 | fn_gen = function(i1, i2) 100 | i2 = i2 or i1 101 | return i1*v1, i2*v2 102 | end 103 | 104 | fn_gen is where the signal is generated; this function is called for each 105 | sample. The Pan module takes one or two inputs, i1 and i2, and generates two 106 | outputs, with the input signal panned left or right, depending on the v1 and v2 constants, which 107 | where calculated in the fn_set() function of the 'pan' control 108 | 109 | }, init) 110 | 111 | The constructor takes an optional 'init' table with initial values for the controls. 112 | 113 | end 114 | 115 | The end. 116 | 117 | -------------------------------------------------------------------------------- /doc/worp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 79 | 89 | 98 | 107 | 118 | 127 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /examples/appregiator.lua: -------------------------------------------------------------------------------- 1 | 2 | j = Jack:new() 3 | 4 | function Synth() 5 | 6 | local o1 = Dsp:Square() 7 | local o2 = Dsp:Saw() 8 | local f = Dsp:Filter { f = 800, Q = 4 } 9 | local adsr = Dsp:Adsr { A = 0.01, D = 0.01, S = 0.3 } 10 | local adsr2 = Dsp:Adsr { A = 0.03, D = 1.00, S = 0.1 } 11 | local pan = Dsp:Pan() 12 | 13 | local depth = 0 + 0.1 14 | 15 | local function instr(note, vel) 16 | if vel > 0 then depth = vel end 17 | local freq = n2f(note) 18 | o1:set { f = freq } 19 | o2:set { f = freq * 4.0 + 0.2 } 20 | adsr:set { vel = vel } 21 | adsr2:set { vel = vel } 22 | pan:set { pan = rr(-0.8, 0.8) } 23 | end 24 | 25 | local function gen() 26 | f:set { f = adsr2() * 3000 * depth + 20} 27 | return pan(0.1 * adsr() * f(o1() + o2() + math.random() * 0.75)) 28 | end 29 | 30 | return instr, gen 31 | end 32 | 33 | 34 | instr, gen = Poly(Synth) 35 | 36 | rev = Dsp:Reverb { damp = 0.2 } 37 | 38 | j:dsp("synth", 0, 2, function() 39 | return rev(gen()) 40 | end) 41 | 42 | 43 | j:connect("worp") 44 | 45 | function play2(instr, note, vel, dur) 46 | play(instr, note, vel, dur) 47 | at(0.18*3, play, instr, note - 12, vel * 0.9, dur) 48 | end 49 | 50 | ns = { 34, 22, 70, 34, 65, 34, 17, 74, 36, 72, 53, 58 } 51 | 52 | function appreg(i) 53 | 54 | local n = ns[i] 55 | local v = (i % 5) == 1 and 0.9 or 0.7 56 | play2(instr, n, v, 0.16) 57 | 58 | at(0.18, function() 59 | appreg((i % #ns) + 1) 60 | end) 61 | end 62 | 63 | 64 | appreg(1) 65 | 66 | -- vi: ft=lua ts=3 sw=3 67 | -------------------------------------------------------------------------------- /examples/gen.lua: -------------------------------------------------------------------------------- 1 | 2 | j = Jack:new("worp") 3 | l = Linuxsampler:new("piano", "/opt/samples") 4 | m = Metro:new(150, 10) 5 | v = l:add("piano/megapiano.gig", 0) 6 | n = Dsp:Noise() 7 | nf = Dsp:Filter { type = "hp", f = 8000, Q = 5 } 8 | p = Dsp:Pan() 9 | a = Dsp:Adsr { A = 0, D = 0.03, S = 1, R = 0.05 } 10 | r = Dsp:Reverb { } 11 | f = Dsp:Filter { type = "bp", Q = 5 } 12 | lfo = Dsp:Osc { f = 4 / m:t_meas() } 13 | 14 | ns = { 36, 75, 79, 84, 34, 75, 79, 74, 84, 82, 15 | 36, 75, 79, 34, 84, 75, 79, 74, 84, 82 } 16 | 17 | function loop(i) 18 | play(v, ns[i], ns[i] < 40 and 0.7 or 0.6, 1.0) 19 | i = (i % #ns) + 1 20 | at(m:t_beat(), loop, i) 21 | end 22 | 23 | j:dsp("wah", 1, 1, function(vi) 24 | f:set { f = lfo() * 500 + 700 } 25 | return f(vi) 26 | end) 27 | 28 | 29 | j:dsp("perc", 0, 2, function() 30 | return p( r( nf( a() * n() ) ) ) 31 | end) 32 | 33 | function click() 34 | nf:set { f = rr(8000, 12000) } 35 | p:set { pan = rr(-1, 1) } 36 | a:set { vel = rr(0.2, 0.6) } 37 | at(0.01, function() a:set { vel = 0 } end) 38 | at(m:t_beat() * 0.5, "click") 39 | end 40 | 41 | loop(1) 42 | click() 43 | 44 | j:connect("piano", "worp:wah") 45 | j:connect("worp:wah", "system:playback") 46 | j:connect("worp:perc", "system:playback") 47 | 48 | -- vi: ft=lua ts=3 sw=3 49 | -------------------------------------------------------------------------------- /examples/scales.lua: -------------------------------------------------------------------------------- 1 | 2 | m = Metro:new(60) 3 | 4 | jack = Jack:new("worp") 5 | 6 | linuxsampler = Linuxsampler:new("synth", "/opt/samples") 7 | linuxsampler:reset() 8 | 9 | harp = linuxsampler:add("concert_harp.gig", 2) 10 | violin = linuxsampler:add("violins.gig", 5) 11 | bass = linuxsampler:add("basses.gig", 0) 12 | 13 | function mkchord(min, max, n, ns) 14 | local os = {} 15 | for i = 1, n do 16 | local o = ns[(i % #ns) + 1] 17 | local p = o 18 | while p < min or p >= max do 19 | p = o + math.random(0, 8) * 12 20 | end 21 | os[#os+1] = p 22 | end 23 | table.sort(os) 24 | return os 25 | end 26 | 27 | 28 | function playchord(c) 29 | local d = m:t_beat() * 2 30 | local ms = Chord:new(0, "minor", c) 31 | play(bass, 36+ms[1], 0.7, d) 32 | local ns = mkchord(40, 63, 7, ms) 33 | local vel = rl { 0.5, 0.6 } 34 | for i = 1, #ns do 35 | local n = ns[i] 36 | play(violin, n, vel/4, d) 37 | end 38 | local prog = { 39 | i7 = { 'vii7', 'v7' }, 40 | vii7 = { 'i7' }, 41 | v7 = { 'i7', 'vi7' }, 42 | vi7 = { 'ii7' }, 43 | ii7 = { 'v7', 'vii7' } 44 | } 45 | at(d, "playchord", rl(prog[c])) 46 | end 47 | 48 | 49 | function pulse() 50 | play(harp, rl { 71, 72, 72, 72, 74, 75, 79, }, rl { 0.6, 0.8 }, m:t_beat() / 4 ) 51 | at(m:t_beat() / rl { 1.333333, 2, 4, } , "pulse") 52 | end 53 | 54 | pulse() 55 | 56 | math.randomseed(5) 57 | 58 | m:at_beat("pulse") 59 | m:at_beat("playchord", 'i7') 60 | 61 | jack:connect("synth", "system") 62 | 63 | 64 | -- vi: ft=lua ts=3 sw=3 65 | 66 | -------------------------------------------------------------------------------- /lib/Chord.lua: -------------------------------------------------------------------------------- 1 | 2 | Chord = {} 3 | 4 | local chord_list = { 5 | 6 | -- triad chords 7 | 8 | [""] = { 0, 4, 7 }, -- major 9 | ["-"] = { 0, 3, 7 }, -- minor 10 | ["o" ] = { 0, 3, 6 }, -- diminshed 11 | ["+"] = { 0, 3, 8 }, -- augmented 12 | 13 | -- four note chords 14 | 15 | ["6"] = { 0, 4, 7, 9 }, -- major 6th 16 | ["-6"] = { 0, 3, 7, 9 }, -- minor 6th 17 | ["^7"] = { 0, 4, 7, 11 }, -- major 7th 18 | ["7"] = { 0, 4, 7, 10 }, -- dominant 7th 19 | ["-7"] = { 0, 3, 7, 10 }, -- minor 7th 20 | ["-7b5"] = { 0, 3, 6, 9 }, -- minor 7th ♭5 21 | ["o7"] = { 0, 3, 6, 8 }, -- diminished 7th 22 | ["+7"] = { 0, 4, 6, 8 }, -- augmented 7th 23 | } 24 | 25 | 26 | local diatonic_list = { 27 | major = { 28 | i = { 0, "" }, 29 | i7 = { 0, "^7" }, 30 | ii = { 2, "-" }, 31 | ii7 = { 2, "-7" }, 32 | iii = { 4, "-" }, 33 | iii = { 4, "-7" }, 34 | iv = { 5, "" }, 35 | iv7 = { 5, "^7" }, 36 | v = { 7, "" }, 37 | v7 = { 7, "7" }, 38 | vi = { 9, "-" }, 39 | vi7 = { 9, "-7" }, 40 | vii = { 11, "o" }, 41 | vii7 = { 11, "o7" }, 42 | }, 43 | minor = { 44 | i = { 0, "-" }, 45 | i7 = { 0, "-7" }, 46 | ii = { 2, "o" }, 47 | ii7 = { 2, "o7" }, 48 | iii = { 3, "" }, 49 | iii7 = { 3, "^7" }, 50 | iv = { 5, "-" }, 51 | iv7 = { 5, "-7" }, 52 | v = { 7, "" }, 53 | v7 = { 7, "7" }, 54 | vi = { 8, "" }, 55 | vi7 = { 8, "^7" }, 56 | vii = { 11, "o" }, 57 | vii7 = { 11, "o7" }, 58 | } 59 | } 60 | 61 | 62 | function Chord:new(base, type, degree) 63 | local ns = {} 64 | local i = diatonic_list[type][degree] 65 | for _, n in ipairs(chord_list[i[2]]) do 66 | ns[#ns+1] = base + i[1] + n 67 | end 68 | return ns 69 | end 70 | 71 | -- vi: ft=lua ts=3 sw=3 72 | 73 | -------------------------------------------------------------------------------- /lib/Core.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- The Core library defines some handy low level functions: 4 | -- 5 | -- rl(LIST): return a random item from the given list 6 | -- rr(MIN, MAX): return a random number from the given range 7 | -- ntf(NOTE): return frequency of midi note number NOTE 8 | -- 9 | 10 | function rl(vs) 11 | return vs[math.random(1, #vs)] 12 | end 13 | 14 | 15 | function rr(min, max) 16 | return min + math.random() * (max-min) 17 | end 18 | 19 | 20 | function n2f(n) 21 | return 440 * math.pow(2, (n-57) / 12) 22 | end 23 | 24 | 25 | -- vi: ft=lua ts=3 sw=3 26 | -------------------------------------------------------------------------------- /lib/Dsp/Adsr.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Attack / Decay / Sustain / Release module. 4 | -- 5 | -- This module generates an envelope amplitude between 0.0 and 1.0. When the 6 | -- 'vel' argument is set to >0 the envelope generator will start (note on), 7 | -- when 'vel' is set to zero, the generator will go to the decay phase and fall 8 | -- down to zero amplitude (note off) 9 | -- 10 | 11 | function Dsp:Adsr(init) 12 | 13 | local arg = { 14 | A = 0, 15 | D = 0, 16 | S = 1, 17 | R = 0, 18 | on = true, 19 | } 20 | 21 | local state, v = nil, 0 22 | local velocity = 0 23 | local dv_A, dv_D, dv_R, level_S = 0, 0, 0, 1 24 | local dv = 0 25 | 26 | return Dsp:Mod({ 27 | description = "ADSR envelope generator", 28 | controls = { 29 | { 30 | id = "vel", 31 | description = "Velocity", 32 | fn_set = function(val) 33 | if val > 0 then 34 | velocity = val 35 | state, dv = "A", dv_A 36 | end 37 | if val == 0 then 38 | state, dv = "R", dv_R 39 | end 40 | end 41 | }, { 42 | id = "A", 43 | description = "Attack", 44 | max = 10, 45 | unit = "s", 46 | default = 0.1, 47 | fn_set = function(val) 48 | dv_A = math.min(1/(srate * val), 1) 49 | end, 50 | }, { 51 | id = "D", 52 | description = "Decay", 53 | max = 10, 54 | unit = "s", 55 | default = 0.1, 56 | fn_set = function(val) 57 | dv_D = math.max(-1/(srate * val), -1) 58 | end, 59 | }, { 60 | id = "S", 61 | description = "Sustain", 62 | default = 1, 63 | fn_set = function(val) 64 | level_S = val 65 | end 66 | }, { 67 | id = "R", 68 | description = "Release", 69 | max = 10, 70 | unit = "s", 71 | default = 0.1, 72 | fn_set = function(val) 73 | dv_R = math.max(-1/(srate * val), -1) 74 | end 75 | }, 76 | }, 77 | fn_gen = function() 78 | if state == "A" and v >= 1 then 79 | state, dv = "D", dv_D 80 | elseif state == "D" and v < level_S then 81 | state, dv = "S", 0 82 | end 83 | v = v + dv 84 | v = math.max(v, 0) 85 | v = math.min(v, 1) 86 | return v * velocity 87 | end, 88 | 89 | }, init) 90 | end 91 | 92 | -- vi: ft=lua ts=3 sw=3 93 | -------------------------------------------------------------------------------- /lib/Dsp/Const.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Generator which outputs a constant value in the range 0..1, controlled by 4 | -- the 'c' control. Useful for easy mapping of a GUI knob or midi CC to a value. 5 | -- 6 | 7 | function Dsp:Const(init) 8 | 9 | local c 10 | 11 | return Dsp:Mod({ 12 | description = "Const", 13 | controls = { 14 | { 15 | id = "c", 16 | description = "Value", 17 | default = 1, 18 | fmt = "%0.2f", 19 | fn_set = function(val) c = val end, 20 | }, 21 | }, 22 | 23 | fn_gen = function() 24 | return c 25 | end 26 | 27 | }, init) 28 | 29 | end 30 | 31 | -- vi: ft=lua ts=3 sw=3 32 | -------------------------------------------------------------------------------- /lib/Dsp/Control.lua: -------------------------------------------------------------------------------- 1 | 2 | function Dsp:Control(def, mod) 3 | 4 | local control = { 5 | 6 | -- methods 7 | 8 | get = function(control) 9 | return control.value 10 | end, 11 | 12 | set = function(control, value, update) 13 | if control.type == "enum" and type(value) == "number" then 14 | value = control.options[math.floor(value + 0.5)] 15 | end 16 | if value ~= nil then 17 | control.value = value 18 | for fn in pairs(control.fn_set) do 19 | fn(value) 20 | end 21 | if update ~= false then 22 | mod:update() 23 | end 24 | end 25 | end, 26 | 27 | set_uni = function(control, v, update) 28 | if control.min and control.max then 29 | v = control.min + v * (control.max-control.min) 30 | if control.log then v = (control.max+1) ^ (v/control.max) - 1 end 31 | control:set(v) 32 | end 33 | end, 34 | 35 | on_set = function(control, fn) 36 | control.fn_set[fn] = true 37 | end, 38 | 39 | -- data 40 | 41 | id = def.id or "", 42 | description = def.description or "", 43 | type = def.type or "number", 44 | fmt = def.fmt, 45 | min = def.min or 0, 46 | max = def.max or 1, 47 | options = def.options or {}, 48 | log = def.log, 49 | unit = def.unit, 50 | default = def.default or 0, 51 | value = nil, 52 | fn_set = {}, 53 | } 54 | 55 | if def.type == "enum" then 56 | control.min, control.max = 1, #def.options 57 | end 58 | 59 | setmetatable(control, { 60 | __tostring = function() 61 | return "control:%s(%s)" % { control.id, control.value } 62 | end 63 | }) 64 | 65 | if def.fn_set then 66 | control:on_set(def.fn_set) 67 | end 68 | control:set(def.default, false) 69 | 70 | return control 71 | 72 | end 73 | 74 | 75 | -- vi: ft=lua ts=3 sw=3 76 | -------------------------------------------------------------------------------- /lib/Dsp/Filter.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- The Filter module is a basic audio filter with configurable frequency, resonance and gain. 4 | -- A number of different filter types are provided: 5 | -- 6 | -- * hp: High pass 7 | -- * lp: Low pass 8 | -- * bp: Band pass 9 | -- * bs: Band stop (aka, Notch) 10 | -- * ls: Low shelf 11 | -- * hs: High shelf 12 | -- * ap: All pass 13 | -- * eq: Peaking EQ filter 14 | -- 15 | -- The code is based on a document from Robert Bristow-Johnson, check the original 16 | -- at [http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt] for more details 17 | -- about the filter implementation 18 | -- 19 | 20 | function Dsp:Filter(init) 21 | 22 | local fs = 44100 23 | local a0, a1, a2, b0, b1, b2 24 | local x0, x1, x2 = 0, 0, 0 25 | local y0, y1, y2 = 0, 0, 0 26 | 27 | local type, f, Q, gain 28 | 29 | return Dsp:Mod({ 30 | description = "Biquad filter", 31 | controls = { 32 | fn_update = function() 33 | local w0 = 2 * math.pi * (f / fs) 34 | local alpha = math.sin(w0) / (2*Q) 35 | local cos_w0 = math.cos(w0) 36 | local A = math.pow(10, gain/40) 37 | 38 | if type == "hp" then 39 | b0, b1, b2 = (1 + cos_w0)/2, -(1 + cos_w0), (1 + cos_w0)/2 40 | a0, a1, a2 = 1 + alpha, -2*cos_w0, 1 - alpha 41 | 42 | elseif type == "lp" then 43 | b0, b1, b2 = (1 - cos_w0)/2, 1 - cos_w0, (1 - cos_w0)/2 44 | a0, a1, a2 = 1 + alpha, -2*cos_w0, 1 - alpha 45 | 46 | elseif type == "bp" then 47 | b0, b1, b2 = Q*alpha, 0, -Q*alpha 48 | a0, a1, a2 = 1 + alpha, -2*cos_w0, 1 - alpha 49 | 50 | elseif type == "bs" then 51 | b0, b1, b2 = 1, -2*cos_w0, 1 52 | a0, a1, a2 = 1 + alpha, -2*cos_w0, 1 - alpha 53 | 54 | elseif type == "ls" then 55 | local ap1, am1, tsAa = A+1, A-1, 2 * math.sqrt(A) * alpha 56 | local am1_cos_w0, ap1_cos_w0 = am1 * cos_w0, ap1 * cos_w0 57 | b0, b1, b2 = A*( ap1 - am1_cos_w0 + tsAa ), 2*A*( am1 - ap1_cos_w0 ), A*( ap1 - am1_cos_w0 - tsAa ) 58 | a0, a1, a2 = ap1 + am1_cos_w0 + tsAa, -2*( am1 + ap1_cos_w0 ), ap1 + am1_cos_w0 - tsAa 59 | 60 | elseif type == "hs" then 61 | local ap1, am1, tsAa = A+1, A-1, 2 * math.sqrt(A) * alpha 62 | local am1_cos_w0, ap1_cos_w0 = am1 * cos_w0, ap1 * cos_w0 63 | b0, b1, b2 = A*( ap1 + am1_cos_w0 + tsAa ), -2*A*( am1 + ap1_cos_w0 ), A*( ap1 + am1_cos_w0 - tsAa ) 64 | a0, a1, a2 = ap1 - am1_cos_w0 + tsAa, 2*( am1 - ap1_cos_w0 ), ap1 - am1_cos_w0 - tsAa 65 | 66 | elseif type == "eq" then 67 | b0, b1, b2 = 1 + alpha*A, -2*cos_w0, 1 - alpha*A 68 | a0, a1, a2 = 1 + alpha/A, -2*cos_w0, 1 - alpha/A 69 | 70 | elseif type == "ap" then 71 | b0, b1, b2 = 1 - alpha, -2*cos_w0, 1 + alpha 72 | a0, a1, a2 = 1 + alpha, -2*cos_w0, 1 - alpha 73 | 74 | else 75 | error("Unsupported filter type " .. type) 76 | end 77 | end, 78 | { 79 | id = "type", 80 | description = "Filter type", 81 | type = "enum", 82 | options = { "lp", "hp", "bp", "bs", "ls", "hs", "eq", "ap" }, 83 | default = "lp", 84 | fn_set = function(val) type = val end 85 | }, { 86 | id = "f", 87 | description = "Frequency", 88 | max = 20000, 89 | log = true, 90 | unit = "Hz", 91 | default = 440, 92 | fn_set = function(val) f = val end 93 | }, { 94 | id = "Q", 95 | description = "Resonance", 96 | min = 0.1, 97 | max = 100, 98 | log = true, 99 | default = 1, 100 | fn_set = function(val) Q = val end 101 | }, { 102 | id = "gain", 103 | description = "Shelf/EQ filter gain", 104 | min = -60, 105 | max = 60, 106 | unit = "dB", 107 | default = 0, 108 | fn_set = function(val) gain = val end 109 | } 110 | }, 111 | 112 | fn_gen = function(x0) 113 | y2, y1 = y1, y0 114 | y0 = (b0 / a0) * x0 + (b1 / a0) * x1 + (b2 / a0) * x2 - (a1 / a0) * y1 - (a2 / a0) * y2 115 | x2, x1 = x1, x0 116 | return y0 117 | end 118 | }, init) 119 | 120 | end 121 | 122 | -- vi: ft=lua ts=3 sw=3 123 | -------------------------------------------------------------------------------- /lib/Dsp/Mod.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | function Dsp:Mod(def, init) 4 | 5 | local mod = { 6 | 7 | -- methods 8 | 9 | update = function(mod) 10 | if def.controls.fn_update then 11 | def.controls.fn_update() 12 | end 13 | end, 14 | 15 | set = function(mod, id, value) 16 | if type(id) == "table" then 17 | for id, value in pairs(id) do 18 | mod:control(id):set(value, false) 19 | end 20 | mod:update() 21 | else 22 | mod:control(id):set(value) 23 | end 24 | end, 25 | 26 | controls = function(mod) 27 | return mod.control_list 28 | end, 29 | 30 | get = function(mod) 31 | return mod 32 | end, 33 | 34 | control = function(mod, id) 35 | return mod.control_list[id] 36 | end, 37 | 38 | help = function(mod) 39 | print("%s:" % { mod.description }) 40 | for _, control in ipairs(mod:controls()) do 41 | local range = "" 42 | if #control.options > 0 then 43 | range = table.concat(control.options, "/") 44 | elseif control.min or control.max then 45 | range = "%s..%s" % { control.min or 0, control.max or 1 } 46 | end 47 | print(" - %s: %s (%s)" % { control.id, control.description, range }) 48 | end 49 | end, 50 | 51 | -- data 52 | 53 | id = def.id, 54 | description = def.description, 55 | control_list = {} 56 | } 57 | 58 | setmetatable(mod, { 59 | __tostring = function() 60 | return "mod:%s" % { def.description } 61 | end, 62 | __call = function(_, ...) 63 | return def.fn_gen(...) 64 | end, 65 | }) 66 | 67 | for i, def in ipairs(def.controls) do 68 | local control = Dsp:Control(def, mod) 69 | mod.control_list[i] = control 70 | mod.control_list[control.id] = control 71 | if init then 72 | control:set(init[control.id], false) 73 | end 74 | end 75 | 76 | mod:update() 77 | 78 | return mod 79 | end 80 | 81 | -- vi: ft=lua ts=3 sw=3 82 | -------------------------------------------------------------------------------- /lib/Dsp/Noise.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Random noise generator module, generates noise in the range -1.0 .. +1.0 4 | -- 5 | -- The noise module can generate both uniform and gaussian white noise. 6 | -- 7 | -- The gaussian noise is based on code from ..., check the original document at 8 | -- [http://www.taygeta.com/random/gaussian.html] for more details about the 9 | -- implementation 10 | -- 11 | 12 | function Dsp:Noise(init) 13 | 14 | local random, sqrt, log = math.random, math.sqrt, math.log 15 | local type, y1, y2 16 | 17 | local function rand() 18 | return 2 * random() - 1 19 | end 20 | 21 | return Dsp:Mod({ 22 | description = "Noise generator", 23 | controls = { 24 | { 25 | id = "type", 26 | description = "Noise type", 27 | type = "enum", 28 | options = { "uniform", "gaussian" }, 29 | default = "uniform", 30 | fn_set = function(val) type = val end 31 | }, 32 | }, 33 | 34 | fn_gen = function() 35 | 36 | if type == "uniform" then 37 | return rand() 38 | end 39 | 40 | if type == "gaussian" then 41 | 42 | local x1, x2, w 43 | repeat 44 | x1, x2 = rand(), rand() 45 | w = x1 * x1 + x2 * x2 46 | until w < 1 47 | w = sqrt((-2 * log(w)) / w) 48 | y1 = x1 * w 49 | y2 = x2 * w 50 | return y1 51 | end 52 | end, 53 | 54 | }, init) 55 | 56 | end 57 | 58 | -- vi: ft=lua ts=3 sw=3 59 | -------------------------------------------------------------------------------- /lib/Dsp/Osc.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- The Osc module generates a cosine wave at the given frequency. The output 4 | -- range is -1.0 .. +1.0 5 | -- 6 | 7 | function Dsp:Osc(init) 8 | 9 | local cos = math.cos 10 | local i, di = 0, 0 11 | 12 | return Dsp:Mod({ 13 | id = "osc", 14 | description = "Sine oscillator", 15 | controls = { 16 | { 17 | id = "f", 18 | description = "Frequency", 19 | max = 20000, 20 | log = true, 21 | unit = "Hz", 22 | default = 440, 23 | fn_set = function(val) 24 | di = val * math.pi * 2 / srate 25 | end 26 | }, 27 | }, 28 | fn_gen = function() 29 | i = i + di 30 | return cos(i) 31 | end 32 | }, init) 33 | end 34 | 35 | -- vi: ft=lua ts=3 sw=3 36 | -------------------------------------------------------------------------------- /lib/Dsp/Pan.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Stereo pan. Takes one or two inputs and pans them between the two outputs. 4 | -- 5 | 6 | function Dsp:Pan(init) 7 | 8 | local v1, v2 9 | 10 | return Dsp:Mod({ 11 | description = "Pan", 12 | controls = { 13 | { 14 | id = "pan", 15 | description = "Pan", 16 | min = -1, 17 | max = 1, 18 | default = 0, 19 | fn_set = function(val) 20 | v1 = math.min(1 + val, 1) 21 | v2 = math.min(1 - val, 1) 22 | end, 23 | }, 24 | }, 25 | 26 | fn_gen = function(i1, i2) 27 | i2 = i2 or i1 28 | return i1*v1, i2*v2 29 | end 30 | 31 | }, init) 32 | 33 | end 34 | 35 | -- vi: ft=lua ts=3 sw=3 36 | -------------------------------------------------------------------------------- /lib/Dsp/Pitchshift.lua: -------------------------------------------------------------------------------- 1 | 2 | function Dsp:Pitchshift(init) 3 | 4 | local pr, prn, pw = 0, 0, 0 5 | local size = 0x10000 6 | local buf = {} 7 | local nmix = 50 8 | local mix = 0 9 | local dmax = 1200 10 | local win = 250 11 | local step = 10 12 | local factor = 1 13 | 14 | local floor = math.floor 15 | 16 | for i = 0, size do buf[i] = 0 end 17 | 18 | local function wrap(i) 19 | return i % size 20 | end 21 | 22 | local function read(i) 23 | return buf[floor(wrap(i))] 24 | end 25 | 26 | local function read4(fi) 27 | local i = floor(fi) 28 | local f = fi - i 29 | local a, b, c, d = read(i-1), read(i), read(i+1), read(i+2) 30 | local c_b = c-b 31 | return b + f * ( c_b - 0.16667 * (1.-f) * ( (d - a - 3*c_b) * f + (d + 2*a - 3*b))) 32 | end 33 | 34 | local function find(pf, pt1, pt2) 35 | 36 | local cmax, ptmax = 0, pt1 37 | 38 | for pt = pt1, pt2-win, step do 39 | local c = 0 40 | for i = 0, win-1, step do 41 | c = c + read(pf+i) * read(pt+i) 42 | end 43 | if c > cmax then 44 | cmax = c 45 | ptmax = pt 46 | end 47 | end 48 | 49 | return ptmax 50 | end 51 | 52 | return Dsp:Mod({ 53 | description = "Pitch shift", 54 | controls = { 55 | { 56 | id = "f", 57 | description = "Factor", 58 | default = 1, 59 | min = 0.5, 60 | max = 2, 61 | fmt = "%0.2f", 62 | fn_set = function(val) factor = val end, 63 | }, 64 | }, 65 | 66 | fn_gen = function(vi) 67 | 68 | if vi == "factor" then 69 | factor = arg 70 | return 71 | end 72 | 73 | buf[pw] = vi 74 | local vo = read4(pr) 75 | 76 | if mix > 0 then 77 | local f = mix / nmix 78 | vo = vo * f + read4(prn) * (1-f) 79 | mix = mix - 1 80 | if mix == 0 then pr = prn end 81 | end 82 | 83 | if mix == 0 then 84 | local d = (pw - pr) % size 85 | if factor < 1 then 86 | if d > dmax then 87 | mix = nmix 88 | prn = find(pr, pr+dmax/2, pr+dmax) 89 | end 90 | else 91 | if d < win or d > dmax * 2 then 92 | mix = nmix 93 | prn = find(pr, pr-dmax, pr-win) 94 | end 95 | end 96 | end 97 | 98 | pw = wrap(pw + 1) 99 | pr = wrap(pr + factor) 100 | prn = wrap(prn + factor) 101 | 102 | return vo 103 | end 104 | }, init) 105 | 106 | end 107 | 108 | -- vi: ft=lua ts=3 sw=3 109 | -------------------------------------------------------------------------------- /lib/Dsp/Reverb.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Freeverb reverberator, based on Jezar's public domain C++ sources. 4 | -- 5 | -- This is a relatively simple and cheap stereo reverberator, based on a 6 | -- cascade of comb and allpass filters. 7 | -- 8 | 9 | function Dsp:Reverb(init) 10 | 11 | local function allpass(bufsize) 12 | local buffer = {} 13 | local feedback = 0 14 | local bufidx = 0 15 | return function(input) 16 | local bufout = buffer[bufidx] or 0 17 | local output = -input + bufout 18 | buffer[bufidx] = input + (bufout*feedback) 19 | bufidx = (bufidx + 1) % bufsize 20 | return output 21 | end 22 | end 23 | 24 | local comb_fb = 0 25 | local comb_damp1 = 0.5 26 | local comb_damp2 = 0.5 27 | 28 | local function fcomb(bufsize, feedback, damp) 29 | local buffer = {} 30 | local bufidx = 0 31 | local filterstore = 0 32 | return function(input) 33 | local output = buffer[bufidx] or 0 34 | local filterstore = (output*comb_damp2) + (filterstore*comb_damp1) 35 | buffer[bufidx] = input + (filterstore*comb_fb) 36 | bufidx = (bufidx + 1) % bufsize 37 | return output 38 | end 39 | end 40 | 41 | local fixedgain = 0.015 42 | local scalewet = 3 43 | local scaledry = 2 44 | local scaledamp = 0.4 45 | local scaleroom = 0.28 46 | local offsetroom = 0.7 47 | local stereospread = 23 48 | 49 | local comb, allp 50 | local gain 51 | local dry, wet1, wet2 52 | 53 | local comb, allp = { 54 | { 55 | fcomb(1116), fcomb(1188), 56 | fcomb(1277), fcomb(1356), 57 | fcomb(1422), fcomb(1491), 58 | fcomb(1557), fcomb(1617), 59 | }, { 60 | fcomb(1116+stereospread), fcomb(1188+stereospread), 61 | fcomb(1277+stereospread), fcomb(1356+stereospread), 62 | fcomb(1422+stereospread), fcomb(1491+stereospread), 63 | fcomb(1557+stereospread), fcomb(1617+stereospread), 64 | } 65 | }, { 66 | { 67 | allpass(556), allpass(441), allpass(341), allpass(225), 68 | }, { 69 | allpass(556+stereospread), allpass(441+stereospread), 70 | allpass(341+stereospread), allpass(225+stereospread), 71 | } 72 | } 73 | 74 | local arg_wet, arg_dry, arg_room, arg_damp 75 | 76 | return Dsp:Mod({ 77 | description = "Reverb", 78 | controls = { 79 | fn_update = function() 80 | local initialroom = arg_room 81 | local initialdamp = arg_damp 82 | local initialwet = arg_wet/scalewet 83 | local initialdry = arg_dry or 0 84 | local initialwidth = 2 85 | local initialmode = 0 86 | 87 | local wet = initialwet * scalewet 88 | local roomsize = (initialroom*scaleroom) + offsetroom 89 | dry = initialdry * scaledry 90 | local damp = initialdamp * scaledamp 91 | local width = initialwidth 92 | local mode = initialmode 93 | 94 | wet1 = wet*(width/2 + 0.5) 95 | wet2 = wet*((1-width)/2) 96 | 97 | comb_fb = roomsize 98 | comb_damp1 = damp 99 | comb_damp2 = 1 - damp 100 | gain = fixedgain 101 | 102 | end, 103 | { 104 | id = "wet", 105 | description = "Wet volume", 106 | default = 0.5, 107 | fmt = "%0.2f", 108 | fn_set = function(val) arg_wet = val end 109 | }, { 110 | id = "dry", 111 | description = "Dry volume", 112 | default = 0.5, 113 | fmt = "%0.2f", 114 | fn_set = function(val) arg_dry = val end 115 | }, { 116 | id = "room", 117 | description = "Room size", 118 | max = 1.1, 119 | default = 0.5, 120 | fmt = "%0.2f", 121 | fn_set = function(val) arg_room = val end 122 | }, { 123 | id = "damp", 124 | description = "Damping", 125 | default = 0.5, 126 | fmt = "%0.2f", 127 | fn_set = function(val) arg_damp = val end 128 | } 129 | }, 130 | fn_gen = function(in1, in2) 131 | in2 = in2 or in1 132 | local input = (in1 + in2) * gain 133 | 134 | local out = { 0, 0 } 135 | 136 | for c = 1, 2 do 137 | for i = 1, #comb[c] do 138 | out[c] = out[c] + comb[c][i](input) 139 | end 140 | for i = 1, #allp[c] do 141 | out[c] = allp[c][i](out[c]) 142 | end 143 | end 144 | 145 | local out1 = out[1]*wet1 + out[2]*wet2 + in1*dry 146 | local out2 = out[2]*wet1 + out[1]*wet2 + in2*dry 147 | 148 | return out1, out2 149 | end 150 | }, init) 151 | 152 | end 153 | 154 | -- vi: ft=lua ts=3 sw=3 155 | -------------------------------------------------------------------------------- /lib/Dsp/Saw.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- The Saw module generates a sawtooth wave at the given frequency. The output 4 | -- range is -1.0 .. +1.0 5 | -- 6 | 7 | function Dsp:Saw(init) 8 | 9 | local v, dv = 0, 0 10 | 11 | return Dsp:Mod({ 12 | description = "Saw tooth oscillator", 13 | controls = { 14 | { 15 | id = "f", 16 | description = "Frequency", 17 | max = 20000, 18 | log = true, 19 | unit = "Hz", 20 | default = 440, 21 | fn_set = function(val) 22 | dv = 2 * val / srate 23 | end, 24 | }, 25 | }, 26 | fn_gen = function() 27 | v = v + dv 28 | if v > 1 then v = v - 2 end 29 | return v 30 | end 31 | }, init) 32 | 33 | end 34 | 35 | -- vi: ft=lua ts=3 sw=3 36 | -------------------------------------------------------------------------------- /lib/Dsp/Square.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- The Square module generates a square wave at the given frequency and pwm 4 | -- offset. The output range is -1.0 .. +1.0 5 | -- 6 | 7 | function Dsp:Square(init) 8 | 9 | local saw = Dsp:Saw() 10 | local pwm = 0.5 11 | 12 | return Dsp:Mod({ 13 | id = "osc", 14 | description = "Sine oscillator", 15 | controls = { 16 | { 17 | id = "f", 18 | description = "Frequency", 19 | max = 20000, 20 | log = true, 21 | unit = "Hz", 22 | default = 440, 23 | fn_set = function(val) 24 | saw:set { f = val } 25 | end 26 | }, { 27 | id = "pwm", 28 | description = "PWM", 29 | default = 0, 30 | fn_set = function(val) 31 | pwm = val 32 | end 33 | }, 34 | }, 35 | fn_gen = function() 36 | return saw() < pwm and -1 or 1 37 | end 38 | }, init) 39 | end 40 | 41 | -- vi: ft=lua ts=3 sw=3 42 | -------------------------------------------------------------------------------- /lib/Dsp/Width.lua: -------------------------------------------------------------------------------- 1 | 2 | function Dsp:Width(init) 3 | 4 | local a1, a2 5 | 6 | return Dsp:Mod({ 7 | description = "Width", 8 | controls = { 9 | { 10 | id = "width", 11 | description = "Stereo width", 12 | min = 0, 13 | max = 2, 14 | default = 1, 15 | fn_set = function(v) 16 | a1 = math.min(0.5 + v/2, 1) 17 | a2 = 0.5 - v/2 18 | end, 19 | }, 20 | }, 21 | 22 | fn_gen = function(i1, i2) 23 | return i1 * a1 + i2 * a2, 24 | i2 * a1 + i1 * a2 25 | end 26 | 27 | }, init) 28 | 29 | end 30 | 31 | -- vi: ft=lua ts=3 sw=3 32 | -------------------------------------------------------------------------------- /lib/Fluidsynth.lua: -------------------------------------------------------------------------------- 1 | 2 | Fluidsynth = {} 3 | 4 | local ffi = require("ffi") 5 | 6 | local fs = ffi.load("fluidsynth") 7 | 8 | ffi.cdef [[ 9 | typedef struct fluid_settings_t *fluid_settings_t; 10 | typedef struct fluid_synth_t * fluid_synth_t; 11 | typedef struct fluid_audio_driver_t *fluid_audio_driver_t; 12 | fluid_settings_t *new_fluid_settings(void); 13 | int fluid_settings_setstr(fluid_settings_t* settings, const char *name, const char *str); 14 | int fluid_settings_setint(fluid_settings_t* settings, const char *name, int val); 15 | int fluid_settings_setnum(fluid_settings_t* settings, const char *name, double val); 16 | fluid_synth_t* new_fluid_synth(fluid_settings_t* settings); 17 | fluid_audio_driver_t* new_fluid_audio_driver(fluid_settings_t* settings, fluid_synth_t* synth); 18 | int fluid_synth_sfload(fluid_synth_t* synth, const char* filename, int reset_presets); 19 | int fluid_synth_all_notes_off(fluid_synth_t* synth, int chan); 20 | int fluid_synth_program_change(fluid_synth_t* synth, int chan, int program); 21 | int fluid_synth_noteon(fluid_synth_t* synth, int chan, int key, int vel); 22 | int fluid_synth_noteoff(fluid_synth_t* synth, int chan, int key, int vel); 23 | ]] 24 | 25 | 26 | -- 27 | -- Send note off to all channels 28 | -- 29 | 30 | local function reset(fluidsynth) 31 | for i = 1, 16 do 32 | fs.fluid_synth_all_notes_off(fluidsynth.synth, i); 33 | end 34 | end 35 | 36 | 37 | -- 38 | -- Add channel with given program, return instrument function 39 | -- 40 | 41 | local function add(fluidsynth, prog) 42 | 43 | local ch 44 | if prog == 127 then 45 | ch = 9 46 | else 47 | prog = prog or 9 48 | ch = fluidsynth.channels 49 | fluidsynth.channels = fluidsynth.channels + 1 50 | fs.fluid_synth_program_change(fluidsynth.synth, ch, prog) 51 | end 52 | 53 | return function(note, vel) 54 | if vel > 0 then 55 | fs.fluid_synth_noteon(fluidsynth.synth, ch, note, vel * 127); 56 | else 57 | fs.fluid_synth_noteoff(fluidsynth.synth, ch, note, vel * 127); 58 | end 59 | end 60 | end 61 | 62 | 63 | -- 64 | -- Create fluidsynth instance, create and attach jack client with the given 65 | -- name 66 | -- 67 | 68 | 69 | function Fluidsynth:new(name, fname) 70 | 71 | name = name or "Fluidsynth" 72 | 73 | local fluidsynth = { 74 | 75 | -- methods 76 | 77 | reset = reset, 78 | add = add, 79 | 80 | -- data 81 | 82 | channels = 0, 83 | } 84 | 85 | local settings = fs.new_fluid_settings() 86 | fs.fluid_settings_setstr(settings, "audio.jack.id", name) 87 | fluidsynth.synth = fs.new_fluid_synth(settings); 88 | local adriver = fs.new_fluid_audio_driver(settings, fluidsynth.synth); 89 | fs.fluid_synth_sfload(fluidsynth.synth, fname, 1); 90 | 91 | return fluidsynth 92 | 93 | end 94 | 95 | -- vi: ft=lua ts=3 sw=3 96 | 97 | -------------------------------------------------------------------------------- /lib/Gui.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- See the [gui] documentation page 4 | -- 5 | 6 | Gui = {} 7 | 8 | 9 | -- 10 | -- GUI process. This is a forked subprocess handling the GTK gui 11 | -- 12 | 13 | local lgi, GLib, Gtk, Gdk 14 | 15 | local function random_id() 16 | return "%08x" % math.random(0, 0xffffffff) 17 | end 18 | 19 | -- 20 | -- Implementation of knob widget 21 | -- 22 | 23 | local function Knob(parm) 24 | 25 | local size = 40 26 | local w, h 27 | local cx = size * 0.5 28 | local cy = size * 0.5 29 | local r = size * 0.45 30 | local value = 0 31 | local def = parm.default 32 | local min = parm.lower 33 | local max = parm.upper 34 | local range = max - min 35 | local drag_x, drag_y, dragging = 0, 0, false 36 | 37 | local da = Gtk.DrawingArea { 38 | width = size, 39 | height = size, 40 | can_focus = true, 41 | } 42 | 43 | local knob = { 44 | 45 | da = da, 46 | 47 | set_value = function(knob, v) 48 | v = math.max(v, min) 49 | v = math.min(v, max) 50 | value = v 51 | da.window:invalidate_rect(nil) 52 | parm.on_value_changed(knob) 53 | end, 54 | 55 | get_value = function(knob) 56 | return value 57 | end 58 | } 59 | 60 | local sc = da:get_style_context() 61 | 62 | function da:on_draw(cr) 63 | 64 | local function v2a(v) 65 | return (v - min) / range * 5 + 2.25 66 | end 67 | 68 | local function l(v, r1, r2) 69 | local a = v2a(v) 70 | local x, y = math.cos(a), math.sin(a) 71 | cr:move_to(cx + x * r * r1, cy + y * r * r1) 72 | cr:line_to(cx + x * r * r2, cy + y * r * r2) 73 | cr:stroke() 74 | end 75 | 76 | cr:set_line_width(1) 77 | 78 | cr:arc(cx, cy, r, 0, 6.28) 79 | cr:set_source_rgb(0.7, 0.7, 0.7) 80 | cr:fill_preserve() 81 | cr:set_source_rgb(0.9, 0.9, 0.9) 82 | cr:stroke() 83 | 84 | cr:arc(cx, cy, r*0.7, 0, 6.28) 85 | cr:set_source_rgb(0.5, 0.5, 0.5) 86 | cr:fill_preserve() 87 | cr:set_source_rgb(0.3, 0.3, 0.3) 88 | cr:stroke() 89 | 90 | cr:set_line_width(2) 91 | local a1, a2 = v2a(0), v2a(value) 92 | cr:arc(cx, cy, r*0.85, math.min(a1, a2), math.max(a1, a2)) 93 | cr:set_source_rgb(0.0, 1.0, 0.0) 94 | cr:stroke() 95 | 96 | cr:set_line_width(2) 97 | cr:set_source_rgb(0.0, 0.0, 0.0) 98 | l(min, 0.7, 0.9) 99 | l(max, 0.7, 0.9) 100 | if def ~= min and def ~= max then 101 | l(def, 0.8, 0.9) 102 | end 103 | 104 | cr:set_line_width(3) 105 | cr:set_source_rgb(1.0, 1.0, 1.0) 106 | l(value, 0, 0.6) 107 | 108 | cr:set_line_width(1) 109 | 110 | if da.has_focus then 111 | Gtk.render_focus(sc, cr, 0, 0, w, h) 112 | end 113 | 114 | end 115 | 116 | function da:on_configure_event(event) 117 | w, h = self.allocation.width, self.allocation.height 118 | cx, cy = w/2, h/2 119 | return true 120 | end 121 | 122 | function da:on_scroll_event(event) 123 | local d = 0 124 | if event.direction == 'UP' then d = parm.page_increment end 125 | if event.direction == 'DOWN' then d = -parm.page_increment end 126 | if event.state.SHIFT_MASK then d = d / 10 end 127 | if event.state.CONTROL_MASK then d = d * 10 end 128 | knob:set_value(value + d) 129 | end 130 | 131 | function da:on_button_press_event(event) 132 | da.has_focus = true 133 | end 134 | 135 | function da:on_motion_notify_event(event) 136 | local _, x, y, state = event.window:get_device_position(event.device) 137 | 138 | if not dragging and state.BUTTON1_MASK then dragging, drag_x, drag_y = true, x, y end 139 | if dragging and not state.BUTTON1_MASK then dragging = false end 140 | 141 | if dragging then 142 | local dy = drag_y - y 143 | knob:set_value(value + dy * range / 100) 144 | da.window:invalidate_rect(nil) 145 | drag_y = y 146 | end 147 | end 148 | 149 | function da:on_key_press_event(event) 150 | local d = 0 151 | local k = event.keyval 152 | if k == Gdk.KEY_Up then d = parm.step_increment end 153 | if k == Gdk.KEY_Down then d = -parm.step_increment end 154 | if k == Gdk.KEY_Page_Up then d = parm.page_increment end 155 | if k == Gdk.KEY_Page_Down then d = -parm.page_increment end 156 | if event.state.SHIFT_MASK then d = d / 10 end 157 | if event.state.CONTROL_MASK then d = d * 10 end 158 | knob:set_value(value + d) 159 | end 160 | 161 | da:add_events(Gdk.EventMask { 162 | 'SCROLL_MASK', 163 | 'KEY_PRESS_MASK', 164 | 'BUTTON_PRESS_MASK', 165 | 'LEAVE_NOTIFY_MASK', 166 | 'BUTTON_PRESS_MASK', 167 | 'POINTER_MOTION_MASK', 168 | 'POINTER_MOTION_HINT_MASK' }) 169 | 170 | return knob 171 | 172 | end 173 | 174 | 175 | 176 | local cmd_handler = { 177 | 178 | new = function(worker, id, data) 179 | 180 | local window = Gtk.Window { 181 | resizable = false, 182 | title = data.gui_id, 183 | Gtk.Box { 184 | id = "box", 185 | valign = 'START', 186 | orientation = 'HORIZONTAL', 187 | { 188 | Gtk.Label { 189 | use_markup = true, 190 | label = data.label and "" .. data.label .. "" or nil, 191 | }, 192 | } 193 | } 194 | } 195 | 196 | worker.gui_list[data.gui_id] = { 197 | window = window, 198 | group_list = {}, 199 | } 200 | window:show_all() 201 | 202 | end, 203 | 204 | 205 | add_group = function(worker, id, data) 206 | 207 | local gui = worker.gui_list[data.gui_id] 208 | local info = data.info 209 | local mute = false 210 | 211 | local frame = Gtk.Frame { 212 | label = data.label, 213 | margin = 4, 214 | Gtk.Grid { 215 | margin = 4, 216 | column_spacing = 4, 217 | id = data.group_id, 218 | } 219 | } 220 | 221 | frame:set_label_align(0.5, 1) 222 | 223 | gui.window.child.box:add(frame) 224 | 225 | gui.window:show_all() 226 | 227 | gui.group_list[data.group_id] = { 228 | index = 0, 229 | control_list = {} 230 | } 231 | 232 | end, 233 | 234 | 235 | add_control = function(worker, id, data) 236 | 237 | local gui = worker.gui_list[data.gui_id] 238 | local group = gui.group_list[data.group_id] 239 | 240 | local window = gui.window 241 | local grid = window.child[data.group_id] 242 | 243 | local x = group.index 244 | group.index = group.index + 1 245 | 246 | local control = data.control 247 | local mute = false 248 | local fn_set = function(v) end 249 | 250 | grid:add { 251 | left_attach = x, top_attach = 0, 252 | Gtk.Label { 253 | label = control.id, 254 | halign = 'CENTER', 255 | } 256 | } 257 | 258 | if control.type == "number" then 259 | 260 | local label = Gtk.Label { 261 | halign = 'CENTER', 262 | } 263 | 264 | local function k2v(v) 265 | if control.log then 266 | if v < 0.001 then v = 0.001 end 267 | v = (control.max+1) ^ (v/control.max) - 1 268 | end 269 | return v 270 | end 271 | 272 | local function v2k(v) 273 | if control.log then 274 | v = (control.max) * math.log(v+1) / math.log(control.max) 275 | end 276 | return v 277 | end 278 | 279 | local function on_value_changed(knob) 280 | local val = k2v(knob:get_value()) 281 | if not mute then 282 | worker:tx { cmd = "set", data = { 283 | uid = data.uid, 284 | value = val, 285 | }} 286 | end 287 | local unit = control.unit 288 | if val > 1000 then 289 | val = val / 1000 290 | unit = "k" .. unit 291 | end 292 | local fmt = "%d" 293 | if math.abs(val) < 100 then fmt = "%.1f" end 294 | if math.abs(val) < 10 then fmt = "%.2f" end 295 | if control.unit then 296 | fmt = fmt .. " " .. unit 297 | end 298 | label:set_text((control.fmt or fmt) % val) 299 | end 300 | 301 | local knob = Knob { 302 | lower = v2k(control.min), 303 | upper = v2k(control.max), 304 | default = v2k(control.default), 305 | step_increment = (control.max-control.min)/100, 306 | page_increment = (control.max-control.min)/10, 307 | on_value_changed = on_value_changed, 308 | } 309 | 310 | grid:add { 311 | left_attach = x, top_attach = 1, 312 | knob.da 313 | } 314 | 315 | grid:add { 316 | left_attach = x, top_attach = 2, 317 | label, 318 | } 319 | 320 | fn_set = function(val) 321 | mute = true 322 | knob:set_value(v2k(val)) 323 | mute = false 324 | end 325 | 326 | 327 | elseif control.type == "enum" then 328 | 329 | local combo = Gtk.ComboBoxText { 330 | on_changed = function(s) 331 | worker:tx { cmd = "set", data = { 332 | uid = data.uid, 333 | value = s:get_active_text(), 334 | }} 335 | end 336 | } 337 | 338 | grid:add { 339 | left_attach = x, top_attach = 1, 340 | combo 341 | } 342 | 343 | for i, o in ipairs(control.options) do 344 | combo:append(i-1, o) 345 | end 346 | 347 | fn_set = function(v) 348 | for i, o in ipairs(control.options) do 349 | if v == o then combo:set_active(i-1) end 350 | end 351 | end 352 | 353 | elseif control.type == "bool" then 354 | 355 | local switch = Gtk.ToggleButton { 356 | label = " ", 357 | on_toggled = function(s) 358 | worker:tx { cmd = "set", data = { 359 | uid = data.uid, 360 | value = s:get_active() 361 | }} 362 | end 363 | } 364 | 365 | grid:add { 366 | left_attach = x, top_attach = 1, 367 | switch, 368 | } 369 | 370 | fn_set = function(v) 371 | switch:set_active(v) 372 | end 373 | 374 | end 375 | 376 | group.control_list[control.id] = { 377 | control = control, 378 | fn_set = fn_set, 379 | } 380 | 381 | window:show_all() 382 | end, 383 | 384 | 385 | set_control = function(worker, id, data) 386 | 387 | local gui = worker.gui_list[data.gui_id] 388 | local group = gui.group_list[data.group_id] 389 | local control = group.control_list[data.control_id] 390 | control.fn_set(data.value) 391 | 392 | end 393 | 394 | } 395 | 396 | 397 | local function handle_msg(worker, code) 398 | local fn, err = load("return " .. code, "msg") 399 | if fn then 400 | local ok, msg = safecall(fn) 401 | if ok then 402 | local h = cmd_handler[msg.cmd] 403 | if h then 404 | h(worker, msg.modid, msg.data) 405 | end 406 | else 407 | logf(LG_WRN, "ipc run error: %s", data, code) 408 | end 409 | else 410 | logf(LG_WRN, "ipc syntax error: %s", err) 411 | end 412 | end 413 | 414 | 415 | local function gui_main(fd) 416 | 417 | P.signal(P.SIGINT, nil) 418 | 419 | lgi = require "lgi" 420 | GLib, Gtk, Gdk = lgi.GLib, lgi.Gtk, lgi.Gdk 421 | 422 | local worker = { 423 | 424 | -- methods 425 | 426 | tx = function(worker, msg) 427 | P.send(fd, serialize(msg)) 428 | end, 429 | 430 | -- data 431 | 432 | fd = fd, 433 | gui_list = {} 434 | } 435 | 436 | -- Main GTK loop: receive messages from main process and handle GUI 437 | 438 | local ioc = GLib.IOChannel.unix_new(worker.fd) 439 | GLib.io_add_watch(ioc, 1, 1, function(a, b, c ,d) 440 | local code, err = P.recv(worker.fd, 65535) 441 | if code then 442 | handle_msg(worker, code) 443 | end 444 | return true 445 | end) 446 | 447 | Gtk.main() 448 | 449 | end 450 | 451 | 452 | -- 453 | -- Main process 454 | -- 455 | 456 | 457 | local function group_add_control(group, control, uid, fn_set) 458 | 459 | group.gui.Gui:tx { cmd = "add_control", data = { 460 | gui_id = group.gui.id, 461 | group_id = group.id, 462 | control = control, 463 | uid = uid, 464 | }} 465 | 466 | local function set(val) 467 | group.gui.Gui:tx { cmd = "set_control", data = { 468 | gui_id = group.gui.id, 469 | group_id = group.id, 470 | control_id = control.id, 471 | value = val 472 | }} 473 | end 474 | 475 | control:on_set(set) 476 | 477 | if control.value then 478 | set(control.value) 479 | end 480 | 481 | --group.gui.fn_set[id] = fn_set 482 | end 483 | 484 | 485 | local function gui_add_group(gui, label) 486 | 487 | local group = { 488 | 489 | -- methods 490 | 491 | add_control = group_add_control, 492 | 493 | -- data 494 | 495 | gui = gui, 496 | label = label, 497 | id = random_id(), 498 | } 499 | 500 | 501 | gui.Gui:tx { cmd = "add_group", data = { 502 | gui_id = gui.id, 503 | label = group.label, 504 | group_id = group.id }} 505 | 506 | return group 507 | 508 | end 509 | 510 | 511 | local function gui_start(Gui) 512 | local s1, s2 = P.socketpair(P.AF_UNIX, P.SOCK_DGRAM, 0) 513 | Gui.pid = P.fork() 514 | if Gui.pid == 0 then 515 | for i = 3, 255 do 516 | if i ~= s2 then P.close(i) end 517 | end 518 | gui_main(s2) 519 | end 520 | P.close(s2) 521 | Gui.s = s1 522 | Gui.uid_to_control = {} 523 | 524 | watch_fd(s1, function() 525 | local code = P.recv(s1, 65535) 526 | local fn, err = load("return " .. code) 527 | if fn then 528 | local ok, msg = safecall(fn) 529 | if ok then 530 | if msg.cmd == "set" then 531 | local control = Gui.uid_to_control[msg.data.uid] 532 | if control then 533 | control:set(msg.data.value) 534 | end 535 | end 536 | else 537 | logf(LG_WRN, "ipc error: %s", data) 538 | end 539 | else 540 | logf(LG_WRN, "ipc error: %s", err) 541 | end 542 | end) 543 | 544 | Gui.tx = function(_, msg) 545 | P.send(Gui.s, serialize(msg)) 546 | end 547 | end 548 | 549 | 550 | local function gui_add_mod(gui, gen, label) 551 | 552 | local controls = gen:controls() 553 | local group = gui:add_group(label or gen.description) 554 | for _, control in ipairs(controls) do 555 | local uid = random_id() 556 | gui.Gui.uid_to_control[uid] = control 557 | group:add_control(control, uid, function(v) 558 | gen:set { [control.id] = v } 559 | end) 560 | end 561 | end 562 | 563 | 564 | function Gui:new(label) 565 | 566 | if not Gui.pid then 567 | gui_start(Gui) 568 | end 569 | 570 | local gui = { 571 | 572 | -- methods 573 | 574 | add_group = gui_add_group, 575 | add_mod = gui_add_mod, 576 | 577 | -- data 578 | 579 | id = random_id(), 580 | Gui = Gui, 581 | 582 | } 583 | 584 | gui.Gui:tx { cmd = "new", data = { gui_id = gui.id, label = label } } 585 | 586 | return gui 587 | 588 | end 589 | 590 | 591 | -- vi: ft=lua ts=3 sw=3 592 | -------------------------------------------------------------------------------- /lib/Jack.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- See the [jack] documentation page 4 | -- 5 | 6 | local jack_c = require "jack_c" 7 | 8 | -- 9 | -- Register jack audio ports, n_in input ports and n_out output ports. 10 | -- Processing is done in given fn, all input data is given as arguments, all 11 | -- returned data is passed as output data. 12 | -- 13 | 14 | local function jack_dsp(jack, name, n_in, n_out, fn) 15 | 16 | local group = jack.group_list[name] 17 | 18 | if not group then 19 | 20 | group = { 21 | fn = fn, 22 | fn_ok = function() end 23 | } 24 | jack.group_list[name] = group 25 | 26 | local t = 0 27 | local dt = 1/jack.srate 28 | local gu, fd = jack_c.add_group(jack.j, name, n_in, n_out) 29 | 30 | -- This function gets called when the jack thread needs more data. The 31 | -- many calls into C should probably be optimized at some time 32 | 33 | watch_fd(fd, function() 34 | P.read(fd, 1) 35 | local ok = safecall(function() 36 | for i = 1, jack.bsize do 37 | jack_c.write(gu, group.fn(jack_c.read(gu))) 38 | t = t + dt 39 | end 40 | end) 41 | if not ok then 42 | print("Restoring last known good function") 43 | group.fn = group.fn_ok 44 | end 45 | end) 46 | 47 | else 48 | group.fn_ok = group.fn 49 | group.fn = fn 50 | end 51 | 52 | end 53 | 54 | 55 | 56 | -- 57 | -- Register jack midi port with given name. Received midi events are passed to 58 | -- callback function 59 | -- 60 | 61 | local function jack_midi(jack, name) 62 | 63 | name = name or "midi" 64 | 65 | if jack.midi_list[name] then 66 | return jack.midi_list[name] 67 | end 68 | 69 | local midi = { 70 | 71 | -- methods 72 | 73 | on_note = function(midi, ch, fn) 74 | midi.fn_note[ch] = midi.fn_note[ch] or {} 75 | midi.fn_note[ch][fn] = true 76 | end, 77 | 78 | on_cc = function(midi, ch, nr, fn) 79 | midi.fn_cc[ch] = midi.fn_cc[ch] or {} 80 | midi.fn_cc[ch][nr] = midi.fn_cc[ch][nr] or {} 81 | midi.fn_cc[ch][nr][fn] = true 82 | end, 83 | 84 | map_instr = function(midi, ch, instr, mapping) 85 | local t, fn_map = type(mapping) 86 | if t == "number" then 87 | fn_map = function(v) return v + mapping end 88 | elseif t == "table" then 89 | fn_map = function(v) return mapping[v] end 90 | elseif t == "function" then 91 | fn_map = mapping 92 | else 93 | fn_map = function(v) return v end 94 | end 95 | 96 | midi:on_note(ch, function(note, vel) 97 | instr(fn_map(note), vel/127) 98 | end) 99 | end, 100 | 101 | map_control = function(midi, ch, nr, control) 102 | midi:on_cc(ch, nr, function(v) 103 | control:set_uni(v) 104 | end) 105 | end, 106 | 107 | map_mod = function(midi, ch, nr, mod) 108 | for i, control in ipairs(mod:controls()) do 109 | midi:map_control(ch, nr+i-1, control) 110 | end 111 | end, 112 | 113 | -- data 114 | 115 | fd = jack_c.add_midi(jack.j, name), 116 | fn_note = {}, 117 | fn_cc = {}, 118 | } 119 | 120 | watch_fd(midi.fd, function() 121 | local msg = P.read(midi.fd, 3) 122 | 123 | local b1, b2, b3 = string.byte(msg, 1, #msg) 124 | logf(LG_DMP, "%02x %02x %02x", b1, b2, b3) 125 | 126 | local t = bit.band(b1, 0xf0) 127 | local ch = bit.band(b1, 0x0f) + 1 128 | 129 | if t == 0x80 or t == 0x90 then 130 | if midi.fn_note[ch] then 131 | for fn in pairs(midi.fn_note[ch]) do 132 | fn(b2, t == 0x90 and b3 or 0) 133 | end 134 | end 135 | elseif t == 0xb0 then 136 | if midi.fn_cc[ch] and midi.fn_cc[ch][b2] then 137 | for fn in pairs(midi.fn_cc[ch][b2] or {}) do 138 | fn(b3/127) 139 | end 140 | end 141 | end 142 | 143 | end) 144 | 145 | jack.midi_list[name] = midi 146 | return midi 147 | 148 | end 149 | 150 | 151 | -- 152 | -- Connect jack ports. All ports matching patt1 are connected to ports matching 153 | -- patt2. If patt2 is not given, it defaults to '*' 154 | -- 155 | 156 | local function jack_conn(jack, patt1, patt2, connect) 157 | 158 | patt2 = patt2 or "*" 159 | logf(LG_DBG, "Connect %s -> %s", patt1, patt2) 160 | 161 | local function find(ps, patt) 162 | local l = {} 163 | for _, p in ipairs(ps) do 164 | local client = p.name:match("[^:]+") 165 | local dir = p.input and "input" or "output" 166 | if patt == '*' or p.name:find(patt, 1, true) then 167 | l[p.type] = l[p.type] or {} 168 | l[p.type][dir] = l[p.type][dir] or {} 169 | l[p.type][dir][client] = l[p.type][dir][client] or {} 170 | table.insert(l[p.type][dir][client], p) 171 | end 172 | end 173 | return l 174 | end 175 | 176 | -- Find all ports matching given patterns, index by type/direction/client 177 | 178 | local ps = jack_c.list_ports(jack.j) 179 | local l1 = find(ps, patt1) 180 | local l2 = find(ps, patt2) 181 | 182 | -- Iterate all types/directions/clients on patt1 183 | 184 | for t, ds in pairs(l1) do 185 | for d1, cs in pairs(ds) do 186 | for c1, ps1 in pairs(cs) do 187 | 188 | -- find and connect matching ports from patt2 189 | 190 | local d2 = (d1 == "input") and "output" or "input" 191 | if l2[t] and l2[t][d2] then 192 | for c2, ps2 in pairs(l2[t][d2]) do 193 | if c2 ~= c1 then 194 | for i = 1, math.max(#ps1, #ps2) do 195 | local p1 = ps1[math.min(i, #ps1)] 196 | local p2 = ps2[math.min(i, #ps2)] 197 | if p1.input then p1,p2 = p2,p1 end 198 | logf(LG_DBG, " %s -> %s", p1.name, p2.name) 199 | if connect then 200 | jack_c.connect(jack.j, p1.name, p2.name) 201 | else 202 | jack_c.disconnect(jack.j, p1.name, p2.name) 203 | end 204 | end 205 | end 206 | end 207 | end 208 | end 209 | end 210 | end 211 | 212 | end 213 | 214 | 215 | local function jack_connect(jack, p1, p2) 216 | jack_conn(jack, p1, p2, true) 217 | end 218 | 219 | 220 | local function jack_disconnect(jack, p1, p2) 221 | jack_conn(jack, p1, p2, false) 222 | end 223 | 224 | -- 225 | -- Create new jack client with the given name 226 | -- 227 | 228 | Jack = {} 229 | 230 | function Jack:new(_, name) 231 | 232 | local j, srate, bsize = jack_c.open(name or "worp") 233 | 234 | local jack = { 235 | 236 | -- methods 237 | 238 | dsp = jack_dsp, 239 | midi = jack_midi, 240 | connect = jack_connect, 241 | disconnect = jack_disconnect, 242 | 243 | -- data 244 | 245 | j = j, 246 | srate = srate, 247 | bsize = bsize, 248 | group_list = {}, 249 | midi_list = {}, 250 | 251 | } 252 | 253 | return jack 254 | 255 | end 256 | 257 | -- vi: ft=lua ts=3 sw=3 258 | 259 | -------------------------------------------------------------------------------- /lib/Linuxsampler.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- 4 | -- The Linuxsampler library provides a simple method to connect to a running 5 | -- Linuxsample instance. Example: 6 | -- 7 | -- l = Linuxsampler:new("synth", "/opt/samples") 8 | -- piano = l:add("piano/Bosendorfer.gig", 0) 9 | -- play(piano, 60, 1) 10 | -- 11 | -- Create a new linuxsampler connection. The given JACK_NAME is used for 12 | -- it's audio ports, the SAMPLE_PATH is an optional directory prefix 13 | -- for sample files: 14 | -- 15 | -- l = Linuxsampler:new(JACK_NAME, SAMPLE_PATH) 16 | -- 17 | -- Create in instrument function, using the given sample and linuxsampler instrument ID: 18 | -- 19 | -- piano = l:add(SAMPLE_FNAME, INSTRUMENT_NUMBER) 20 | -- 21 | 22 | 23 | Linuxsampler = {} 24 | 25 | -- 26 | -- Simple LSCP interface to configure and play linuxsampler. The code below 27 | -- implements a asynchronous interface to make sure not to block the main 28 | -- thread while waiting for linuxsampler to reply. 29 | -- 30 | 31 | -- 32 | -- Execute a linuxsampler command and returns the results. This function 33 | -- assumes the caller is running in a coroutine if an answer is required 34 | -- 35 | 36 | local function cmd(ls, data) 37 | 38 | logf(LG_DMP, "tx> %s", data) 39 | P.send(ls.fd, data .. "\n") 40 | ls.rx_buf = "" 41 | 42 | if coroutine.running() then 43 | ls.rx_fn = resumer() 44 | return coroutine.yield() 45 | else 46 | ls.rx_fn = function() end 47 | end 48 | end 49 | 50 | 51 | -- 52 | -- Add a linuxsampler channel and load the instrument from the given file and 53 | -- index, return instrument function 54 | -- 55 | 56 | local function add(ls, fname, index) 57 | 58 | fname = ls.path .. "/" .. fname 59 | 60 | local ch = ls:cmd("ADD CHANNEL") 61 | ls:cmd("LOAD ENGINE gig %s" % ch) 62 | ls:cmd("SET CHANNEL AUDIO_OUTPUT_DEVICE %s %s" % { ch, ls.audio_dev }) 63 | logf(LG_INF, "Loading instrument %s...", fname) 64 | ok, err = ls:cmd("LOAD INSTRUMENT %q %s %s" % { fname, index or 0, ch }) 65 | if not ok then logf(LG_WRN, "%s", err) end 66 | 67 | local info = ls:cmd("GET CHANNEL INFO %s" % ch) 68 | local inst = info:match("INSTRUMENT_NAME: ([^\n\r]+)") or "-" 69 | logf(LG_INF, " channel %s: %s", ch, inst) 70 | 71 | ls.channel_list[ch] = info 72 | 73 | return function(key, vel) 74 | if vel > 0 then 75 | ls:cmd("SEND CHANNEL MIDI_DATA NOTE_ON %s %s %s\n" % { ch, key, vel * 127 }) 76 | else 77 | ls:cmd("SEND CHANNEL MIDI_DATA NOTE_OFF %s %s %s\n" % { ch, key, vel * 127 }) 78 | end 79 | end 80 | 81 | end 82 | 83 | 84 | -- 85 | -- Send noteoff to all active channels 86 | -- 87 | 88 | local function reset(ls) 89 | for c in pairs(ls.channel_list) do 90 | ls:cmd("SEND CHANNEL MIDI_DATA CC %s 120 0" % c) 91 | end 92 | end 93 | 94 | 95 | -- 96 | -- Create new linuxsampler connection and create jack audio client 97 | -- with the given name. 98 | -- 99 | 100 | function Linuxsampler:new(name, path) 101 | 102 | local fd, err = P.socket(P.AF_INET, P.SOCK_STREAM, 0) 103 | if not fd then 104 | return logf(LG_WRN, "Error connecting to linuxsampler: %s", err) 105 | end 106 | 107 | local ok, err = P.connect(fd, { family = P.AF_INET, addr = "127.0.0.1", port = 8888 }) 108 | if not ok then 109 | return logf(LG_WRN, "Error connecting to linuxsampler: %s", err) 110 | end 111 | 112 | local ls = { 113 | 114 | -- methods 115 | 116 | add = add, 117 | cmd = cmd, 118 | reset = reset, 119 | 120 | -- data 121 | 122 | name = name, 123 | path = path or "", 124 | fd = fd, 125 | tx_queue = {}, 126 | channel_list = {}, 127 | rx_buf = "", 128 | 129 | } 130 | 131 | watch_fd(fd, function() 132 | local data = P.recv(fd, 1024) 133 | if not data then 134 | logf(LG_WRN, "Connection to linuxsampler lost") 135 | return false 136 | end 137 | for l in data:gmatch("([^\r\n]*)[\n\r]+") do 138 | logf(LG_DMP, "rx> %s", l) 139 | local rv = l:match("^OK%[?(%d*)%]?") or l:match("^%.") or l:match("^([%d,]*)$") 140 | local err = l:match("ERR:(.+)") or l:match("WRN:(.+)") 141 | if rv then 142 | ls.rx_fn(#ls.rx_buf > 0 and ls.rx_buf or rv) 143 | elseif err then 144 | ls.rx_fn(nil, err) 145 | else 146 | ls.rx_buf = ls.rx_buf .. l .. "\n" 147 | end 148 | end 149 | end) 150 | 151 | -- Find existing audio dev with given name, or create if needed 152 | 153 | local ds = ls:cmd("LIST AUDIO_OUTPUT_DEVICES") 154 | for d in ds:gmatch("%d+") do 155 | local info = ls:cmd("GET AUDIO_OUTPUT_DEVICE INFO %d" % d) 156 | local name2 = info:match("NAME: '(.-)'") 157 | if name == name2 then 158 | ls.audio_dev = d 159 | end 160 | end 161 | 162 | if not ls.audio_dev then 163 | ls.audio_dev = ls:cmd("CREATE AUDIO_OUTPUT_DEVICE JACK NAME=%q" % name) 164 | ls:cmd("SET AUDIO_OUTPUT_CHANNEL_PARAMETER %d 0 NAME='out_1'" % ls.audio_dev) 165 | ls:cmd("SET AUDIO_OUTPUT_CHANNEL_PARAMETER %d 1 NAME='out_2'" % ls.audio_dev) 166 | end 167 | 168 | on_stop(function() 169 | for c in pairs(ls.channel_list) do 170 | ls:cmd("REMOVE CHANNEL %d" % c) 171 | end 172 | end) 173 | 174 | return ls 175 | 176 | end 177 | 178 | 179 | -- vi: ft=lua ts=3 sw=3 180 | 181 | -------------------------------------------------------------------------------- /lib/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: jack_c.so 3 | 4 | CFLAGS += $(shell pkg-config --cflags jack) 5 | LDFLAGS += $(shell pkg-config --libs jack) 6 | 7 | %.so: %.c 8 | $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) 9 | 10 | clean: 11 | rm -f *.so 12 | -------------------------------------------------------------------------------- /lib/Metro.lua: -------------------------------------------------------------------------------- 1 | 2 | Metro = {} 3 | 4 | function Metro:new(bpm, b) 5 | bpm, b = bpm or 120, b or 4 6 | local spb = 60 / bpm 7 | local spm = spb * b 8 | local spt = spb / 120 9 | return { 10 | t_beat = function() 11 | return spb 12 | end, 13 | t_meas = function() 14 | return spm 15 | end, 16 | at_meas = function(m, ...) 17 | at((math.floor(t_now / spm) + 1) * spm, ...) 18 | end, 19 | at_beat = function(m, ...) 20 | at((math.floor(t_now / spb) + 1) * spb, ...) 21 | end 22 | } 23 | end 24 | 25 | -- vi: ft=lua ts=3 sw=3 26 | 27 | -------------------------------------------------------------------------------- /lib/Mixer.lua: -------------------------------------------------------------------------------- 1 | 2 | Mixer = {} 3 | 4 | local ffi = require("ffi") 5 | 6 | ffi.cdef [[ 7 | typedef void *snd_mixer_t; 8 | typedef void *snd_mixer_elem_t; 9 | typedef void *snd_mixer_class_t; 10 | typedef struct { char foo[64]; } snd_mixer_selem_id_t; 11 | int snd_mixer_open(snd_mixer_t **mixer, int mode); 12 | int snd_mixer_attach(snd_mixer_t *mixer, const char *name); 13 | int snd_mixer_selem_register(snd_mixer_t *mixer, struct snd_mixer_selem_regopt *options, snd_mixer_class_t **classp); 14 | int snd_mixer_load(snd_mixer_t *mixer); 15 | void *malloc(size_t size); 16 | void snd_mixer_selem_id_set_index(snd_mixer_selem_id_t *obj, unsigned int val); 17 | void snd_mixer_selem_id_set_name(snd_mixer_selem_id_t *obj, const char *val); 18 | snd_mixer_elem_t *snd_mixer_find_selem(snd_mixer_t *mixer, const snd_mixer_selem_id_t *id); 19 | int snd_mixer_selem_set_playback_volume_all(snd_mixer_elem_t *elem, long value); 20 | size_t snd_mixer_selem_id_sizeof(void); 21 | int snd_mixer_selem_get_playback_volume_range(snd_mixer_elem_t *elem, long *min, long *max); 22 | ]] 23 | 24 | local alsa = ffi.load("asound") 25 | 26 | local handle = ffi.new("snd_mixer_t *[1]") 27 | local card = "default" 28 | 29 | alsa.snd_mixer_open(handle, 0); 30 | alsa.snd_mixer_attach(handle[0], card); 31 | alsa.snd_mixer_selem_register(handle[0], ffi.NULL, ffi.NULL); 32 | alsa.snd_mixer_load(handle[0]); 33 | 34 | 35 | function Mixer:new(_, name) 36 | 37 | local sid = ffi.new("snd_mixer_selem_id_t *[1]") 38 | sid = ffi.new("snd_mixer_selem_id_t[1]") 39 | 40 | return { 41 | set = function(_, v) 42 | alsa.snd_mixer_selem_id_set_index(sid[0], 0); 43 | alsa.snd_mixer_selem_id_set_name(sid[0], name); 44 | local elem = alsa.snd_mixer_find_selem(handle[0], sid[0]); 45 | local min, max = ffi.new("long[1]", 0), ffi.new("long[1]", 0) 46 | alsa.snd_mixer_selem_get_playback_volume_range(elem, min, max); 47 | v = v * (max[0] - min[0]) + min[0] 48 | alsa.snd_mixer_selem_set_playback_volume_all(elem, v) 49 | end 50 | } 51 | end 52 | 53 | -- vi: ft=lua ts=3 sw=3 54 | 55 | -------------------------------------------------------------------------------- /lib/Poly.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- The Poly function provides an easy way to create polyphonic instruments. 4 | -- 5 | 6 | function Poly(mkinstr, count) 7 | 8 | count = count or 5 9 | 10 | local vs = {} 11 | for i = 1, count do 12 | local vi, vg = mkinstr() 13 | vs[i] = { instr = vi, gen = vg, free = true, age = time() } 14 | end 15 | 16 | local function instr(note, vel) 17 | if vel > 0 then 18 | for i, v in pairs(vs) do 19 | if v.free then 20 | v.note = note 21 | v.instr(note, vel) 22 | v.free = false 23 | break 24 | end 25 | end 26 | else 27 | for i, v in pairs(vs) do 28 | if v.note == note then 29 | v.instr(note, 0) 30 | v.free = true 31 | v.age = time() 32 | end 33 | table.sort(vs, function(a, b) return a.age < b.age end) 34 | end 35 | end 36 | end 37 | 38 | local function gen(note, vel) 39 | local o1, o2 = 0, 0 40 | for _, v in ipairs(vs) do 41 | local i1, i2 = v.gen() 42 | o1 = o1 + (i1 or 0) 43 | o2 = o2 + (i2 or 0) 44 | end 45 | return o1, o2 46 | end 47 | 48 | return instr, gen 49 | end 50 | 51 | -- vi: ft=lua ts=3 sw=3 52 | 53 | -------------------------------------------------------------------------------- /lib/jack_c.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #define MAX_PORTS 4 18 | #define RB_SIZE 16384 19 | 20 | #define sample_t jack_default_audio_sample_t 21 | 22 | 23 | struct port { 24 | int flags; 25 | jack_port_t *port; 26 | jack_ringbuffer_t *rb; 27 | struct port *next; 28 | }; 29 | 30 | 31 | struct group { 32 | int id; 33 | int fd; 34 | char *name; 35 | struct port *port_list; 36 | struct group *next; 37 | }; 38 | 39 | 40 | struct midi { 41 | jack_port_t *port; 42 | int fd; 43 | struct midi *next; 44 | }; 45 | 46 | 47 | struct jack { 48 | int fd; 49 | int direction; 50 | jack_client_t *client; 51 | jack_port_t *port[MAX_PORTS]; 52 | jack_ringbuffer_t *rb[MAX_PORTS]; 53 | jack_port_t *midi_in; 54 | 55 | int group_seq; 56 | struct group *group_list; 57 | struct midi *midi_list; 58 | }; 59 | 60 | 61 | static int process2(jack_nframes_t nframes, void *arg) 62 | { 63 | struct jack *jack = arg; 64 | struct group *group = jack->group_list; 65 | int len = nframes * sizeof(sample_t); 66 | 67 | while(group) { 68 | 69 | int need_data = 0; 70 | 71 | struct port *port = group->port_list; 72 | while(port) { 73 | 74 | jack_ringbuffer_t *rb = port->rb; 75 | jack_port_t *p = port->port; 76 | 77 | if(port->flags == JackPortIsOutput) { 78 | 79 | int avail = jack_ringbuffer_read_space(rb); 80 | 81 | if(avail < len * 3) need_data = 1; 82 | 83 | if(avail >= len) { 84 | sample_t *buf = jack_port_get_buffer(p, nframes); 85 | int r = jack_ringbuffer_read(rb, (void *)buf, len); 86 | if(0 && r != len) printf("underrun\n"); 87 | } 88 | 89 | } 90 | 91 | if(port->flags == JackPortIsInput) { 92 | 93 | if(jack_ringbuffer_write_space(rb) >= len) { 94 | sample_t *buf = jack_port_get_buffer(p, nframes); 95 | int r = jack_ringbuffer_write(rb, (void *)buf, len); 96 | if(0 && r != len) printf("overrun\n"); 97 | } 98 | } 99 | 100 | port = port->next; 101 | } 102 | 103 | if(need_data) { 104 | write(group->fd, " ", 1); 105 | } 106 | 107 | 108 | group = group->next; 109 | } 110 | 111 | struct midi *midi = jack->midi_list; 112 | 113 | while(midi) { 114 | void *buf = jack_port_get_buffer(midi->port, nframes); 115 | int n = jack_midi_get_event_count(buf); 116 | int i; 117 | for(i=0; ifd, ev.buffer, ev.size); 121 | } 122 | midi = midi->next; 123 | } 124 | 125 | return 0; 126 | } 127 | 128 | 129 | static int l_open(lua_State *L) 130 | { 131 | const char *client_name = luaL_checkstring(L, 1); 132 | jack_options_t options = JackNullOption; 133 | jack_status_t status; 134 | 135 | struct jack *jack = lua_newuserdata(L, sizeof *jack); 136 | lua_getfield(L, LUA_REGISTRYINDEX, "jack_c"); 137 | lua_setmetatable(L, -2); 138 | memset(jack, 0, sizeof *jack); 139 | 140 | jack->client = jack_client_open (client_name, options, &status, NULL); 141 | if (jack->client == NULL) { 142 | lua_pushnil(L); 143 | lua_pushfstring(L, "Error creating jack client, status = %x", status); 144 | return 2; 145 | } 146 | 147 | jack_set_process_callback(jack->client, process2, jack); 148 | jack_activate (jack->client); 149 | 150 | lua_pushnumber(L, jack_get_sample_rate(jack->client)); 151 | lua_pushnumber(L, jack_get_buffer_size(jack->client)); 152 | return 3; 153 | } 154 | 155 | 156 | static int l_gc(lua_State *L) 157 | { 158 | struct jack *jack = luaL_checkudata(L, 1, "jack_c"); 159 | jack_client_close(jack->client); 160 | return 0; 161 | } 162 | 163 | 164 | 165 | static int l_add_group(lua_State *L) 166 | { 167 | struct jack *jack = luaL_checkudata(L, 1, "jack_c"); 168 | const char *name = luaL_checkstring(L, 2); 169 | int n_in = luaL_checknumber(L, 3); 170 | int n_out = luaL_checknumber(L, 4); 171 | int i; 172 | int fd[2]; 173 | 174 | pipe(fd); 175 | 176 | char pname[64]; 177 | struct group *group = lua_newuserdata(L, sizeof *group); 178 | lua_getfield(L, LUA_REGISTRYINDEX, "jack_group"); 179 | lua_setmetatable(L, -2); 180 | memset(group, 0, sizeof *group); 181 | 182 | group->name = strdup(name); 183 | group->id = jack->group_seq ++; 184 | group->fd = fd[1]; 185 | 186 | for(i=0; iflags = (i < n_in) ? JackPortIsInput : JackPortIsOutput; 190 | 191 | snprintf(pname, sizeof(pname), "%s-%s-%d", name, (iport = jack_port_register(jack->client, pname, JACK_DEFAULT_AUDIO_TYPE, port->flags, 0); 194 | port->rb = jack_ringbuffer_create(RB_SIZE); 195 | 196 | port->next = group->port_list; 197 | group->port_list = port; 198 | } 199 | 200 | group->next = jack->group_list; 201 | jack->group_list = group; 202 | 203 | lua_pushnumber(L, fd[0]); 204 | return 2; 205 | } 206 | 207 | 208 | static int l_add_midi(lua_State *L) 209 | { 210 | struct jack *jack = luaL_checkudata(L, 1, "jack_c"); 211 | const char *name = luaL_checkstring(L, 2); 212 | int fd[2]; 213 | char pname[64]; 214 | 215 | pipe(fd); 216 | 217 | struct midi *midi = calloc(sizeof *midi, 1); 218 | 219 | snprintf(pname, sizeof(pname), "%s-in", name); 220 | midi->port = jack_port_register(jack->client, pname, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); 221 | midi->fd = fd[1]; 222 | 223 | midi->next = jack->midi_list; 224 | jack->midi_list = midi; 225 | 226 | lua_pushnumber(L, fd[0]); 227 | return 1; 228 | } 229 | 230 | 231 | 232 | static int l_write(lua_State *L) 233 | { 234 | struct group *group = luaL_checkudata(L, 1, "jack_group"); 235 | int n = 2; 236 | 237 | struct port *port = group->port_list; 238 | 239 | while(port) { 240 | if(port->flags == JackPortIsOutput) { 241 | sample_t s = lua_tonumber(L, n++); 242 | if(jack_ringbuffer_write_space(port->rb) >= sizeof s) { 243 | jack_ringbuffer_write(port->rb, (void *)&s, sizeof s); 244 | } 245 | } 246 | port = port->next; 247 | } 248 | 249 | return 0; 250 | } 251 | 252 | 253 | static int l_read(lua_State *L) 254 | { 255 | struct group *group = luaL_checkudata(L, 1, "jack_group"); 256 | int n = 0; 257 | 258 | struct port *port = group->port_list; 259 | 260 | while(port) { 261 | if(port->flags == JackPortIsInput) { 262 | sample_t s = 0; 263 | if(jack_ringbuffer_read_space(port->rb) >= sizeof s) { 264 | jack_ringbuffer_read(port->rb, (void *)&s, sizeof s); 265 | 266 | } 267 | lua_pushnumber(L, s); 268 | n++; 269 | } 270 | port = port->next; 271 | } 272 | 273 | return n; 274 | } 275 | 276 | 277 | static int l_connect(lua_State *L) 278 | { 279 | struct jack *jack = luaL_checkudata(L, 1, "jack_c"); 280 | const char *p1 = luaL_checkstring(L, 2); 281 | const char *p2 = luaL_checkstring(L, 3); 282 | 283 | int r = jack_connect(jack->client, p1, p2); 284 | lua_pushnumber(L, r); 285 | return 1; 286 | } 287 | 288 | 289 | static int l_disconnect(lua_State *L) 290 | { 291 | struct jack *jack = luaL_checkudata(L, 1, "jack_c"); 292 | const char *p1 = luaL_checkstring(L, 2); 293 | const char *p2 = luaL_checkstring(L, 3); 294 | 295 | int r = jack_disconnect(jack->client, p1, p2); 296 | lua_pushnumber(L, r); 297 | return 1; 298 | } 299 | 300 | 301 | static int l_list_ports(lua_State *L) 302 | { 303 | struct jack *jack = luaL_checkudata(L, 1, "jack_c"); 304 | const char **ns = jack_get_ports(jack->client, NULL, NULL, 0); 305 | if(!ns) return 0; 306 | int i, j; 307 | 308 | lua_newtable(L); 309 | for(i=0; ns && ns[i]; i++) { 310 | jack_port_t *p = jack_port_by_name(jack->client, ns[i]); 311 | lua_pushnumber(L, i+1); 312 | lua_newtable(L); 313 | 314 | lua_pushstring(L, ns[i]); lua_setfield(L, -2, "name"); 315 | lua_pushstring(L, jack_port_type(p)); lua_setfield(L, -2, "type"); 316 | 317 | int flags = jack_port_flags(p); 318 | if(flags & JackPortIsInput) { lua_pushboolean(L, 1); lua_setfield(L, -2, "input"); } 319 | if(flags & JackPortIsOutput) { lua_pushboolean(L, 1); lua_setfield(L, -2, "output"); } 320 | if(flags & JackPortIsPhysical) { lua_pushboolean(L, 1); lua_setfield(L, -2, "physical"); } 321 | if(flags & JackPortIsTerminal) { lua_pushboolean(L, 1); lua_setfield(L, -2, "terminal"); } 322 | 323 | lua_newtable(L); 324 | const char **cs = jack_port_get_connections(p); 325 | for(j=0; cs && cs[j]; j++) { 326 | lua_pushnumber(L, j+1); 327 | lua_pushstring(L, cs[j]); 328 | lua_settable(L, -3); 329 | } 330 | free(cs); 331 | lua_setfield(L, -2, "connections"); 332 | 333 | lua_settable(L, -3); 334 | } 335 | return 1; 336 | } 337 | 338 | 339 | static struct luaL_Reg jack_table[] = { 340 | 341 | { "open", l_open }, 342 | { "add_group", l_add_group }, 343 | { "add_midi", l_add_midi }, 344 | 345 | { "write", l_write }, 346 | { "read", l_read }, 347 | { "connect", l_connect }, 348 | { "disconnect", l_disconnect }, 349 | { "list_ports", l_list_ports }, 350 | 351 | { NULL }, 352 | }; 353 | 354 | 355 | int luaopen_jack_c(lua_State *L) 356 | { 357 | luaL_newmetatable(L, "jack_c"); 358 | lua_pushstring(L, "__gc"); 359 | lua_pushcfunction(L, l_gc); 360 | lua_settable(L, -3); 361 | 362 | luaL_newmetatable(L, "jack_group"); 363 | luaL_register(L, "jack_c", jack_table); 364 | return 1; 365 | } 366 | 367 | /* 368 | * End 369 | */ 370 | -------------------------------------------------------------------------------- /lib/jack_c.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zevv/worp/9724431e0cadc014753236d1cf67e0ef310cd073/lib/jack_c.so -------------------------------------------------------------------------------- /test/blip.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Filter test. White noise generator is passed through a CC controlled filter. 4 | -- 5 | 6 | jack = Jack:new("worp") 7 | 8 | 9 | f = Dsp:Filter("lp", 2000, 5) 10 | 11 | d = 0 12 | e = 0 13 | 14 | jack:midi("midi", function(channel, t, d1, d2) 15 | if t == "cc" then 16 | local v = d2 / 127 17 | if d1 == 1 then f("f0", math.exp(v * 10)) end 18 | if d1 == 2 then f("Q", math.pow(2, v*3)) end 19 | if d1 == 3 then f("ft", ({"lp", "hp", "bp", "bs", "ap"})[math.floor(v*4)+1]) end 20 | if d1 == 4 then f("gain", (v - 0.5) * 30) end 21 | if d1 == 5 then d = v end 22 | if d1 == 6 then e = v end 23 | end 24 | end) 25 | 26 | 27 | o = Dsp:Osc(60) 28 | s = Dsp:Saw(90.5) 29 | 30 | print(s) 31 | 32 | r = Dsp:reverb() 33 | 34 | function rl(ns) 35 | return ns[math.random(1, #ns)] 36 | end 37 | 38 | p = 0.5 39 | a2 = 0.02 40 | env = 0 41 | 42 | function rev() 43 | p = math.random() - 0.5 44 | e = math.random(1, 6) 45 | d = math.random() * 0.3 + 0.30 46 | env = 1 47 | at(rl { 1, 2, 3 } * .15, "rev") 48 | a2 = math.random() * 0.03 49 | at(0.15 * rl { 1 }, function() 50 | oo2 = Dsp:osc(math.random(10, 600)) 51 | at(0.15/2 * rl { 1, 2 }, function() 52 | a2 = 0 53 | end) 54 | end) 55 | end 56 | 57 | oo1 = Dsp:osc(9000) 58 | oo2 = Dsp:osc(100) 59 | o2 = function() oo1(oo2() * 2000 + 10000) return oo1() end 60 | --o2 = Dsp:osc(10000) 61 | rev() 62 | 63 | sawvol = 0.04 64 | damp = 0.99995 65 | 66 | jack:dsp("fx", 0, 2, function(t) 67 | local v = env * o() + s() * sawvol 68 | env = env * damp 69 | local w = r(v * d) + v * d 70 | v = f(v + w^math.floor(e*10)) 71 | local vl, vr = p * v * 0.2, v*0.2 * (1-p) 72 | local bip = o2() * a2 73 | vl = vl + bip 74 | vr = vr - bip 75 | return vl, vr 76 | end) 77 | 78 | 79 | jack:connect("worp") 80 | 81 | 82 | -- vi: ft=lua ts=3 sw=3 83 | 84 | -------------------------------------------------------------------------------- /test/filter.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Filter test. White noise generator is passed through a CC controlled filter. 4 | -- 5 | 6 | jack = Jack:new("worp") 7 | 8 | f = Dsp:Filter("bp", 1000, 5) 9 | r = Dsp:Reverb() 10 | 11 | jack:midi("midi", function(channel, t, d1, d2) 12 | if t == "cc" then 13 | local v = d2 / 127 14 | if d1 == 1 then f("f0", math.exp(v * 10)) end 15 | if d1 == 2 then f("Q", math.pow(2, v*3)) end 16 | if d1 == 3 then f("ft", ({"lp", "hp", "bp", "bs", "ap"})[math.floor(v*4)+1]) end 17 | if d1 == 4 then f("gain", (v - 0.5) * 30) end 18 | end 19 | end) 20 | 21 | 22 | jack:dsp("fx", 0, 1, function(t) 23 | return f(math.random()) * 0.1 24 | end) 25 | 26 | jack:connect("worp") 27 | 28 | 29 | -- vi: ft=lua ts=3 sw=3 30 | 31 | -------------------------------------------------------------------------------- /test/gen.lua: -------------------------------------------------------------------------------- 1 | 2 | a = Dsp 3 | 4 | -- 5 | -- DSP test 6 | -- 7 | 8 | jack = Jack:new("worp") 9 | midi = jack:midi("midi") 10 | 11 | -- Voice generator 12 | 13 | o = Dsp:Saw { f = 100 } 14 | f = Dsp:Filter() 15 | lfo = Dsp:Osc { f = 5 } 16 | rev = Dsp:Reverb() 17 | pan = Dsp:Pan() 18 | 19 | gui = Gui:new("Worp") 20 | 21 | n = Dsp:Noise() 22 | c = Dsp:Const() 23 | 24 | midi:map_mod(1, 1, f) 25 | midi:map_mod(1, 5, rev) 26 | 27 | f:help() 28 | 29 | gui:add_mod(o, "Osc") 30 | gui:add_mod(lfo, "LFO") 31 | gui:add_mod(c, "Noise") 32 | gui:add_mod(f) 33 | gui:add_mod(rev) 34 | gui:add_mod(pan) 35 | 36 | jack:dsp("synth", 0, 2, function(i1) 37 | local v = f(o() * (1+n()*c()*10)) * lfo() * 0.1 38 | return pan(rev(v, v)) 39 | end) 40 | 41 | -- Connect ports 42 | 43 | jack:connect("worp") 44 | 45 | -- vi: ft=lua ts=3 sw=3 46 | 47 | -------------------------------------------------------------------------------- /test/looper.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Simple 4 track looper controlled by Akai MKP mini 4 | -- 5 | 6 | jack = Jack:new("worp") 7 | 8 | local function pan(v, p) 9 | return math.max(1 - p, 1) * v, math.max(1 + p, 1) * v 10 | end 11 | 12 | local function looper(t) 13 | 14 | local l = { 15 | 16 | -- methods 17 | 18 | run = function(l, v) 19 | if l.rec then 20 | l.buf[l.ptr] = l.buf[l.ptr] * 0.5 + v 21 | end 22 | l.ptr = (l.ptr + 1) % l.len 23 | local v = l.buf[l.ptr] * l.vol 24 | return pan(v, l.pan) 25 | end, 26 | 27 | -- data 28 | 29 | len = 44800 * t, 30 | buf = {}, 31 | vol = 1, 32 | pan = 1, 33 | ptr = 0, 34 | rec = false, 35 | } 36 | 37 | for i = 0, l.len do 38 | l.buf[i] = 0 39 | end 40 | 41 | return l 42 | 43 | end 44 | 45 | local channels = 4 46 | 47 | local loop = {} 48 | 49 | for i = 1, 8 do 50 | loop[i] = looper(2) 51 | end 52 | 53 | 54 | 55 | local rec = nil 56 | 57 | jack:dsp("looper", 1, 2, function(t, i1) 58 | local o1, o2 = 0, 0 59 | for i = 1,8 do 60 | local t1, t2 = loop[i]:run(i1) 61 | o1 = o1 + t1 62 | o2 = o2 + t2 63 | end 64 | return o1, o2 65 | end) 66 | 67 | 68 | jack:midi("midi", function(channel, t, d1, d2) 69 | 70 | if t == "noteon" or t == "noteoff" then 71 | local onoff = t == "noteon" 72 | local note = d1 73 | if note >= 48 then 74 | local i = note - 47 75 | loop[i].rec = onoff 76 | print("REC", i, onoff) 77 | elseif note >= 44 then 78 | local i = note - 43 79 | print("CLEAR", i) 80 | if onoff then 81 | for j = 1, #loop[i].buf do 82 | loop[i].buf[j] = 0 83 | end 84 | end 85 | end 86 | end 87 | 88 | if t == "cc" then 89 | local v = d2 / 127 90 | if d1 > 4 then 91 | loop[d1-4].pan = v * 2 - 1 92 | else 93 | loop[d1].vol = v 94 | end 95 | end 96 | end) 97 | 98 | jack:connect("worp") 99 | 100 | -- vi: ft=lua ts=3 sw=3 101 | 102 | -------------------------------------------------------------------------------- /test/pitch-shift.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Pitch shifter. Inspired by http://dafx.labri.fr/main/papers/p007.pdf 4 | -- 5 | 6 | jack = Jack:new("worp") 7 | 8 | midi = jack:midi(1) 9 | 10 | gui = Gui:new("worp") 11 | s = Dsp:pitchshift() 12 | 13 | gui:add_mod(s) 14 | midi:map_mod(1, 1, s) 15 | 16 | 17 | jack:dsp("fx", 1, 1, function(t, i) 18 | return s(i) 19 | end) 20 | 21 | 22 | jack:connect("worp") 23 | 24 | 25 | -- vi: ft=lua ts=3 sw=3 26 | 27 | -------------------------------------------------------------------------------- /test/reich.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- Some generated music with effects 4 | -- 5 | 6 | ls = Linuxsampler:new("worp", "/opt/samples") 7 | ls:reset() 8 | 9 | jack = Jack:new() 10 | 11 | piano = ls:add("grand.gig") 12 | violin = ls:add("grand.gig", 0) 13 | 14 | jack:connect("worp") 15 | 16 | function mkchord(min, max, n, ns) 17 | local os = {} 18 | for i = 1, n do 19 | local o = ns[(i % #ns) + 1] 20 | local p = o 21 | while p < min or p >= max do 22 | p = o + math.random(0, 8) * 12 23 | end 24 | os[#os+1] = p 25 | end 26 | table.sort(os) 27 | return os 28 | end 29 | 30 | 31 | math.randomseed(os.time()) 32 | ns = mkchord(40, 83, 5, Chord:new(0, "minor", 'i7')) 33 | 34 | d = { 1, 2, 1, 2, 2, 2, 1 } 35 | 36 | function doe(instr, a, ns, i, dur) 37 | local d = dur / d[i] 38 | play(instr, a + ns[i], i == 1 and 0.7 or 0.5, d/2) 39 | at(d, "doe", instr, a, ns, (i%#ns)+1, dur) 40 | end 41 | 42 | doe(piano, 0, ns, 1, 0.6) 43 | doe(violin, -12, ns, 1, 0.62) 44 | 45 | 46 | 47 | -- vi: ft=lua ts=3 sw=3 48 | 49 | -------------------------------------------------------------------------------- /test/simple-piano.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- A simple midi piano using fluidsynth 4 | -- 5 | 6 | jack = Jack:new("worp") 7 | ls = Linuxsampler:new("synth", "/opt/samples") 8 | 9 | midi = jack:midi() 10 | 11 | piano = ls:add("piano/Bosendorfer.gig", 0) 12 | midi:map_instr(1, piano) 13 | 14 | jack:connect("synth") 15 | jack:connect("worp") 16 | 17 | -- vi: ft=lua ts=3 sw=3 18 | 19 | -------------------------------------------------------------------------------- /test/simple-synth.lua: -------------------------------------------------------------------------------- 1 | 2 | -- 3 | -- A simple polyphonic synth, using DSP code to generate nodes on midi input 4 | -- 5 | 6 | jack = Jack:new("worp") 7 | gui = Gui:new("worp") 8 | fs = Fluidsynth:new("worp", "/usr/share/sounds/sf2/FluidR3_GM.sf2") 9 | 10 | 11 | midi = jack:midi("midi") 12 | 13 | c = Dsp:Const() 14 | gui:add_mod(c) 15 | 16 | midi:map_mod(1, 1, c) 17 | 18 | -- Voice generator module 19 | 20 | function voice() 21 | 22 | local osc = Dsp:Saw() 23 | local filter = Dsp:Filter { type = "lp" } 24 | local adsr = Dsp:Adsr { A = 0.03, D = 0.03, S = 0.6, R = 0.6 } 25 | local adsr2 = Dsp:Adsr { A = 0.3, D = 0.8, S = 0.5, R = 0.6 } 26 | local lfo = Dsp:Osc { f = 6 } 27 | local freq 28 | 29 | return Dsp:Mod({ 30 | id = "synth", 31 | description = "Simple synth", 32 | controls = { 33 | { 34 | id = "f", 35 | description = "Frequency", 36 | max = 20000, 37 | log = true, 38 | unit = "Hz", 39 | default = 440, 40 | fn_set = function(v) 41 | osc:set { f = v } 42 | freq = v 43 | end 44 | }, { 45 | id = "vel", 46 | description = "Velocity", 47 | fn_set = function(v) 48 | adsr:set { vel = v } 49 | adsr2:set { vel = v == 0 and 0 or 1 } 50 | end 51 | }, 52 | }, 53 | fn_gen = function() 54 | filter:set { f = adsr2() * (lfo() * 0.1 + 1) * freq * c() * 5 } 55 | return filter(osc()) * adsr() 56 | end 57 | }, init) 58 | 59 | end 60 | 61 | 62 | v, synth = Dsp:Poly { gen = voice } 63 | 64 | midi:map_instr(1, synth) 65 | 66 | jack:dsp("worp", 0, 1, function() 67 | return v() 68 | end) 69 | 70 | -- Connect ports 71 | 72 | jack:connect("worp") 73 | 74 | -- vi: ft=lua ts=3 sw=3 75 | 76 | -------------------------------------------------------------------------------- /vimplugin.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | package.path = package.path .. ";./lib/?.lua" 4 | package.cpath = package.cpath .. ";./lib/?.so" 5 | local p = require "posix" 6 | 7 | local function send(code) 8 | fd = p.socket(p.AF_INET, p.SOCK_DGRAM, 0) 9 | p.sendto(fd, code, { family = p.AF_INET, addr = "127.0.0.1", port = 9889 }) 10 | p.close(fd) 11 | end 12 | 13 | 14 | function worp(what) 15 | 16 | local b = vim.buffer() 17 | 18 | local from = vim.firstline 19 | local to = vim.lastline 20 | 21 | if what == "stop" then 22 | from, to = 0, 0 23 | send("stop()") 24 | elseif what == "all" then 25 | from = 1 26 | to = #b 27 | elseif what == "paragraph" then 28 | while from > 1 and b[from-1]:find("%S") do 29 | from = from - 1 30 | end 31 | while to < #b and b[to+1]:find("%S") do 32 | to = to + 1 33 | end 34 | elseif what == "function" then 35 | while from > 1 and not b[from]:find("^function") do 36 | from = from - 1 37 | end 38 | while to < #b and not b[to]:find("^end") do 39 | to = to + 1 40 | end 41 | end 42 | 43 | vim.command("sign define sent text=┆ texthl=nonText") 44 | vim.command("sign unplace *") 45 | 46 | if from > 0 then 47 | local code = {} 48 | for i = from, to do 49 | vim.command("sign place " .. i .. " line=" .. i .. " name=sent file=" .. b.fname) 50 | code[#code+1] = b[i] 51 | end 52 | code[#code+1] = "-- live " .. from .. " " .. to .. " " .. (b.name or "") 53 | send(table.concat(code, "\n")) 54 | else 55 | vim.command("sign place 1 line=1 name=sent file=" .. b.fname) 56 | end 57 | 58 | end 59 | 60 | vim.command(':noremap ,a :lua worp("all")') 61 | vim.command(':noremap ,f :lua worp("function")') 62 | vim.command(':noremap ,p :lua worp("paragraph")') 63 | vim.command(':noremap ,v :lua worp("visual")') 64 | vim.command(':noremap ,, :lua worp("stop")') 65 | vim.command(':noremap :lua worp("line")') 66 | 67 | -- vi: ft=lua ts=3 sw=3 68 | -------------------------------------------------------------------------------- /worp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/luajit 2 | 3 | package.path = package.path .. ";./lib/?.lua" 4 | package.cpath = package.cpath .. ";./lib/?.so;./app/?.so" 5 | _ENV = _G 6 | 7 | srate = 44100 8 | 9 | -- 10 | -- Application libraries 11 | -- 12 | 13 | P = require "luaposix" 14 | ffi = require "ffi" 15 | 16 | --require "app.strict" 17 | require "app.log" 18 | require "app.misc" 19 | require "app.evq" 20 | require "app.sandbox" 21 | require "app.co" 22 | require "app.getopt" 23 | require "app.autoload" 24 | 25 | require "lib/Core" 26 | 27 | -- 28 | -- Main 29 | -- 30 | 31 | local opt, arg = getopt(arg, 'l:') 32 | 33 | logf_init(opt.l or "") 34 | 35 | -- 36 | -- Setup sandbox and load any files passed as arguments 37 | -- 38 | 39 | sandbox = Sandbox:new() 40 | 41 | for _, fname in ipairs(arg) do 42 | local fd, err = io.open(fname) 43 | if fd then 44 | sandbox:load(fd:read("*a"), fname .. ":1") 45 | else 46 | logf(LG_WRN, "Could not open %s: %s", fname, err) 47 | end 48 | end 49 | 50 | 51 | -- 52 | -- Open an UDP socket to receive Lua code chunks, and register to the 53 | -- mail loop. 54 | -- 55 | 56 | local s = P.socket(P.AF_INET, P.SOCK_DGRAM, 0) 57 | P.bind(s, { family = P.AF_INET, port = 9889, addr = "127.0.0.1" }) 58 | 59 | watch_fd(s, function() 60 | local code = P.recv(s, 65535) 61 | local from, to, name = 1, 1, "?" 62 | local f, t, n = code:match("\n%-%- live (%d+) (%d+) ([^\n]+)") 63 | if f then from, to, name = f, t, n end 64 | sandbox:load(code, "live " .. name .. ":" .. from) 65 | end) 66 | 67 | 68 | logf(LG_INF, "Ready") 69 | 70 | mainloop() 71 | stop() 72 | 73 | logf(LG_INF, "Bye") 74 | 75 | -- vi: ft=lua ts=3 sw=3 76 | --------------------------------------------------------------------------------