├── .github └── FUNDING.yml ├── lib ├── utils.lua ├── _stdlib.lua ├── _engines.lua ├── textedit.lua ├── keycodes.lua ├── repl.lua ├── _corelib.lua ├── lisp.lua └── tracker.lua ├── NISP.lua └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: its_your_bedtime 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /lib/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | local NOTE_NAMES = {"C", "c", "D", "d", "E", "F", "f", "G", "g", "A", "a", "B"} 4 | local NOTE_VALS = {C = 0, c = 1, D = 2, d = 3, E = 4, F = 5, f = 6, G = 7, g = 8, A = 9, a = 10, B = 11} 5 | 6 | utils.note_num_to_name = function(note_num, include_octave) 7 | local name = NOTE_NAMES[note_num % 12 + 1] 8 | if include_octave then name = name .. math.floor(note_num / 12 - 2) end 9 | return name 10 | end 11 | 12 | utils.note_name_to_num = function(note_str) 13 | local data = {} 14 | for i in string.gmatch(note_str, "(.)") do data[#data + 1 ] = i end 15 | local note = NOTE_VALS[data[1]] or 0 16 | local oct = tonumber(data[2]) or 3 17 | return note + ( oct * 12 ) + 24 18 | end 19 | 20 | utils.join = function(a, b) 21 | for i=2,#b do 22 | table.insert(a, b[i]) 23 | end 24 | end 25 | 26 | utils.startswith = function(text, prefix) 27 | return text:find(prefix, 1, true) == 1 28 | end 29 | 30 | utils.endswith = function(text, suffix) 31 | return text:sub(-string.len(suffix)) == suffix 32 | end 33 | 34 | utils.tostring = function (lst, f) 35 | if type(lst) ~= 'table' then return tostring(lst) end 36 | local str = {} 37 | for _, a in ipairs(lst) do 38 | table.insert(str, utils.tostring(a)) 39 | end 40 | if f then 41 | return string.format("(%s)", table.concat(str, " ")) 42 | else 43 | return string.format("%s", table.concat(str, " ")) 44 | end 45 | end 46 | 47 | utils.log = function(self, st) 48 | if #self.output > 500 then local h,o = 1,{} 49 | for i = 492, 500 do o[h] = self.output[i] 50 | h = h + 1 end self.output = o 51 | end 52 | local str = utils.tostring(st) 53 | local limit = 10 54 | local s = tostring(str):gsub("^%s*(.-)%s*$", "%1") 55 | if string.len(s) == 0 then return false 56 | elseif string.len(s) > 33 then 57 | local b = string.sub(str, 31) 58 | self.output[#self.output] = s 59 | self.output[#self.output + 1] = b 60 | if string.len(b) > 33 then utils.log(self, string.sub(b, 31)) end 61 | else 62 | if s ~= self.output[#self.output] then 63 | self.output[#self.output + 1] = s 64 | end 65 | end 66 | end 67 | 68 | utils.update_offset = function(val, y, length, bounds, offset) 69 | if y > val + (9 - offset) then 70 | val = util.clamp( val + 1, 0, length - bounds) 71 | elseif y < bounds + ( val - (8 - offset)) then 72 | val = util.clamp( val - 1, 0, length - bounds) 73 | end 74 | return val 75 | end 76 | 77 | 78 | utils.tab_key = function (t, e) 79 | local index={} 80 | for k, v in pairs(t) do 81 | if v == e then 82 | return k 83 | end 84 | end 85 | end 86 | 87 | 88 | return utils 89 | -------------------------------------------------------------------------------- /lib/_stdlib.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- standart library 3 | -- @its_your_bedtime 4 | -- 5 | 6 | local stdlib = {} 7 | local utils = include('lib/utils') 8 | 9 | 10 | stdlib['#t'] = true 11 | 12 | stdlib['#f'] = function () return false end 13 | --- math 14 | stdlib['+'] = function(...) local r = 0 for i=1,select("#",...) do r = r + select(i,...) end return r end 15 | 16 | stdlib['-'] = function(...) local r = 0 for i=1,select("#",...) do r = r - select(i,...) end return r end 17 | 18 | stdlib['*'] = function(...) local r = 1 for i=1,select("#",...) do r = r * select(i,...) end return r end 19 | 20 | stdlib['/'] = function(...) local r = 1 for i=1,select("#",...) do r = r / select(i,...) end return r end 21 | 22 | stdlib['%'] = function(...) local r for i=1,(select("#",...) - 1) do r = select(i,...) % select(i + 1,...) end return r end 23 | 24 | stdlib['^'] = function(...) local r for i=1,(select("#",...) - 1) do r = select(i,...) ^ select(i + 1,...) end return r end 25 | -- lists 26 | stdlib['append'] = function(a,b) return end -- to do 27 | 28 | stdlib['apply'] = function(a,b) return a(table.unpack(b)) end 29 | 30 | stdlib['begin'] = function(...) local a = {...}; return a[#a] end 31 | 32 | stdlib['car'] = function(lst) return lst[1] end 33 | 34 | stdlib['cdr'] = function(lst) return table.move(lst, 2, #lst, 1, {}) end 35 | 36 | stdlib['cons'] = function(a, lst) return table.move( lst, 1, #lst, 2, { a }) end 37 | 38 | stdlib['eq'] = function(a, b) return a == b end 39 | 40 | stdlib['='] = function(a, b) return a == b end 41 | 42 | stdlib['>'] = function(a, b) return a > b end 43 | 44 | stdlib['<'] = function(a, b) return a < b end 45 | 46 | stdlib['<='] = function(a, b) return a <= b end 47 | 48 | stdlib['>='] = function(a, b) return a >= b end 49 | 50 | stdlib['list'] = function(...) return {...} end 51 | 52 | stdlib['list?'] = function(a) return type(a) == 'table' end 53 | 54 | stdlib['len'] = function(a) return type(a) == 'table' and #a or string.len(a) end 55 | 56 | stdlib['get'] = function(a, b) return type(a) == 'table' and a[b] end 57 | 58 | stdlib['put'] = function (a, b) return type(a) == 'table' and table.insert(a, b) or (type(a) == 'table' and type(b) == 'table') and join(a, b) end 59 | 60 | stdlib['concat'] = function(...) local r for i=1,(select("#",...) - 1) do r = tostring(select(i,...)):gsub('"','')..tostring(select(i + 1,...)):gsub('"','') end return r end 61 | 62 | stdlib['nil?'] = function(a) return a == nil or #a == 0 end 63 | 64 | stdlib['print'] = function(...) return utils.tostring(..., true) end 65 | 66 | stdlib['num?'] = function(a) return tonumber(a) ~= nil end 67 | 68 | stdlib['map'] = function(fn, a) local v = {}; for i = 1,#a do v[i] = fn(a[i]) end; return v end 69 | 70 | stdlib['rnd'] = function(a, b) return((not a and not b) and math.random(0, 99) or (a and not b) and math.random(a) 71 | or math.random(a, b)) end 72 | 73 | return stdlib -------------------------------------------------------------------------------- /lib/_engines.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- engines 3 | -- @its_your_bedtime 4 | -- 5 | 6 | 7 | local n_engine = {} 8 | local NUM_SAMPLES = 99 9 | 10 | function unrequire(name) 11 | package.loaded[name] = nil 12 | _G[name] = nil 13 | end 14 | 15 | unrequire("timber/lib/timber_engine") 16 | local Timber = include("timber/lib/timber_engine") 17 | engine.name = "Timber" 18 | 19 | 20 | function n_engine.load_folder(file, add) 21 | 22 | local sample_id = 0 23 | if add then 24 | for i = NUM_SAMPLES - 1, 0, -1 do 25 | if Timber.samples_meta[i].num_frames > 0 then 26 | sample_id = i + 1 27 | break 28 | end 29 | end 30 | end 31 | 32 | Timber.clear_samples(sample_id, NUM_SAMPLES - 1) 33 | 34 | local split_at = string.match(file, "^.*()/") 35 | local folder = string.sub(file, 1, split_at) 36 | file = string.sub(file, split_at + 1) 37 | 38 | local found = false 39 | for k, v in ipairs(Timber.FileSelect.list) do 40 | if v == file then found = true end 41 | if found then 42 | if sample_id > 35 then 43 | print("Max files loaded") 44 | break 45 | end 46 | -- Check file type 47 | local lower_v = v:lower() 48 | if string.find(lower_v, ".wav") or string.find(lower_v, ".aif") or string.find(lower_v, ".aiff") then 49 | Timber.load_sample(sample_id, folder .. v) 50 | params:set('play_mode_' .. sample_id, 4) 51 | sample_id = sample_id + 1 52 | else 53 | print("Skipped", v) 54 | end 55 | end 56 | end 57 | end 58 | 59 | 60 | function n_engine.init() 61 | -- timbers 62 | 63 | params:add_trigger('load_f','+ Load Folder') 64 | params:set_action('load_f', function() Timber.FileSelect.enter(_path.audio, function(file) 65 | if file ~= "cancel" then n_engine.load_folder(file, add) end end) end) 66 | 67 | Timber.options.PLAY_MODE_BUFFER_DEFAULT = 3 68 | Timber.options.PLAY_MODE_STREAMING_DEFAULT = 3 69 | params:add_separator() 70 | Timber.add_params() 71 | for i = 0, NUM_SAMPLES - 1 do 72 | local extra_params = { 73 | {type = "option", id = "launch_mode_" .. i, name = "Launch Mode", options = {"Gate", "Toggle"}, default = 1, action = function(value) 74 | Timber.setup_params_dirty = true 75 | end}, 76 | } 77 | params:add_separator() 78 | Timber.add_sample_params(i, true, extra_params) 79 | --params:set('play_mode_' .. i, 4) 80 | --params:set('amp_env_sustain_' .. i, 0) 81 | end 82 | -- softcut 83 | softcut.reset() 84 | audio.level_cut(1) 85 | audio.level_adc_cut(1) 86 | audio.level_eng_cut(1) 87 | for i=1, 6 do 88 | softcut.level(i,1) 89 | softcut.level_input_cut(1, i, 1.0) 90 | softcut.level_input_cut(2, i, 1.0) 91 | softcut.pan(i, 0.5) 92 | softcut.play(i, 0) 93 | softcut.rate(i, 1) 94 | softcut.loop_start(i, 0) 95 | softcut.loop_end(i, 36) 96 | softcut.loop(i, 0) 97 | softcut.rec(i, 0) 98 | softcut.fade_time(i,0.02) 99 | softcut.level_slew_time(i,0.01) 100 | softcut.rate_slew_time(i,0.01) 101 | softcut.rec_level(i, 1) 102 | softcut.pre_level(i, 1) 103 | softcut.position(i, 0) 104 | softcut.buffer(i,1) 105 | softcut.enable(i, 1) 106 | softcut.filter_dry(i, 1) 107 | softcut.filter_fc(i, 0) 108 | softcut.filter_lp(i, 0) 109 | softcut.filter_bp(i, 0) 110 | softcut.filter_rq(i, 0) 111 | end 112 | 113 | 114 | end 115 | 116 | return n_engine -------------------------------------------------------------------------------- /NISP.lua: -------------------------------------------------------------------------------- 1 | -- NISP 2 | -- 3 | -- 4 | -- scheme dialect livecoding 5 | -- tracker for norns 6 | -- 7 | -- 8 | -- @its_your_bedtime 9 | -- 10 | local keyboard = require 'core/keyboard' 11 | local lisp = include("lib/lisp") 12 | local keycodes = include("lib/keycodes") 13 | local fileselect = require('fileselect') 14 | local textentry = require('textentry') 15 | 16 | local kb = {s = {[42] = true, [54] = true }, c = {[29] = true, [125] = true, [127] = true, [97] = true}} 17 | local live = true 18 | local shift = false 19 | local ctrl = false 20 | local metro_main 21 | 22 | function load_project(pth) 23 | if string.find(pth, '.seq') ~= nil then 24 | local saved = tab.load(pth) 25 | if saved ~= nil then 26 | print("data found") 27 | for k,v in pairs(saved[2]) do lisp[k] = v end 28 | lisp.metro:bpm_change(saved[2].bpm) 29 | if saved[1] then params:read(norns.state.data .. saved[1] .. ".pset") end 30 | else 31 | print("no data") 32 | end 33 | end 34 | end 35 | 36 | function save_project(txt) 37 | if txt then 38 | local data = { pat = lisp.pat, bpm = lisp.bpm, div = lisp.div, length = lisp.length, mute = lisp.mute } 39 | tab.save({ txt, data }, norns.state.data .. txt ..".seq") 40 | params:write( norns.state.data .. txt .. ".pset") 41 | else 42 | print("save cancel") 43 | end 44 | end 45 | 46 | function keyboard.code(c, val) 47 | local menu = norns.menu.status() 48 | screen.ping() 49 | lisp.blink = true 50 | shift = keyboard.shift() 51 | ctrl = keyboard.ctrl() 52 | if keyboard.state.GRAVE then lisp.live = not lisp.live return false end 53 | if keyboard.state.ESC then 54 | lisp.live = false 55 | if shift then 56 | norns.menu.toggle(not menu) 57 | elseif menu and not shift then 58 | _norns.key(2, 1) 59 | end 60 | end 61 | if not menu then 62 | lisp.kb_code(c, val) 63 | else 64 | if keyboard.state.LEFT then 65 | if ctrl then _norns.enc(1, -8) else _norns.enc(3, shift and -20 or -2) end 66 | elseif keyboard.state.RIGHT then 67 | if ctrl then _norns.enc(1, 8) else _norns.enc(3, shift and 20 or 2) end 68 | elseif keyboard.state.DOWN then 69 | _norns.enc(2, shift and 104 or 2) 70 | elseif keyboard.state.UP then 71 | _norns.enc(2, shift and -104 or -2) 72 | elseif keyboard.state.ENTER then 73 | _norns.key(3, 1) 74 | end 75 | end 76 | end 77 | 78 | function keyboard.char(k) 79 | local menu = norns.menu.status() 80 | if not menu then 81 | lisp.kb_char(k) 82 | end 83 | end 84 | 85 | function init() 86 | screen.aa(0) 87 | math.randomseed(os.time()) 88 | params:add_trigger('save_p', "< Save project" ) 89 | params:set_action('save_p', function(x) textentry.enter(save_project, 'new') end) 90 | params:add_trigger('load_p', "> Load project" ) 91 | params:set_action('load_p', function(x) fileselect.enter(norns.state.data, load_project) end) 92 | params:add_trigger('new', "+ New" ) 93 | params:set_action('new', function(x) init() end) 94 | params:add_separator() 95 | 96 | lisp.init() 97 | local metro_redraw = metro.init( function() redraw() end, 1 / 30) 98 | metro_redraw:start() 99 | 100 | end 101 | 102 | function enc(n, d) 103 | --if n == 3 then offset = util.clamp(offset + d, 1, 64) end 104 | end 105 | 106 | function redraw() 107 | screen.clear() 108 | lisp.redraw() 109 | screen.update() 110 | end 111 | -------------------------------------------------------------------------------- /lib/textedit.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- simple text editor 3 | -- @its_your_bedtime 4 | -- 5 | 6 | local textedit = { 7 | lines = {{},{},{},{},{},{}, {}}, 8 | pos = {x = 1, y = 1}, 9 | running = false, 10 | evaluated = false, 11 | } 12 | 13 | textedit.open = function(self, t) 14 | self.lines = t or {{},{},{},{},{},{},{}} 15 | end 16 | 17 | textedit.store = function(self) 18 | return self.lines 19 | end 20 | 21 | textedit.buildword = function(self, keyinput) 22 | if keyinput ~= nil then 23 | if self.pos.x ~= 1 then 24 | table.insert(self.lines[self.pos.y] , (#self.lines[self.pos.y] + self.pos.x) , keyinput) 25 | if #self.lines[self.pos.y] > 32 then 26 | local l = self.lines[self.pos.y][#self.lines[self.pos.y]] 27 | table.remove(self.lines[self.pos.y], #self.lines[self.pos.y]) 28 | table.insert(self.lines[self.pos.y + 1], 1, l) 29 | end 30 | else 31 | table.insert(self.lines[self.pos.y], keyinput) 32 | end 33 | if #self.lines[self.pos.y] > 32 then 34 | self.pos.y = util.clamp(self.pos.y + 1, 1, 7) 35 | self.pos.x = 1 36 | end 37 | end 38 | end 39 | 40 | textedit.rm = function(self, back) 41 | if back then 42 | if (#self.lines[self.pos.y] + self.pos.x) <= #self.lines[self.pos.y] then 43 | table.remove(self.lines[self.pos.y], util.clamp((#self.lines[self.pos.y] + self.pos.x), 0, #self.lines[self.pos.y])) 44 | self.pos.x = self.pos.x + 1 45 | end 46 | else 47 | if (#self.lines[self.pos.y] + self.pos.x) > 1 then 48 | table.remove(self.lines[self.pos.y], util.clamp((#self.lines[self.pos.y] + self.pos.x) - 1, 0, #self.lines[self.pos.y])) 49 | end 50 | if #self.lines[self.pos.y] + self.pos.x == 1 then 51 | self.pos.y = util.clamp(self.pos.y - 1, 1, #self.lines) 52 | end 53 | end 54 | end 55 | 56 | textedit.kb_code = function(self, c, val) 57 | if keyboard.state.UP then 58 | self.pos.y = util.clamp(self.pos.y - 1, 1, #self.lines) 59 | elseif keyboard.state.LEFT then 60 | self.pos.x = util.clamp(self.pos.x - 1, (-#self.lines[self.pos.y] + 1) , 1) 61 | elseif keyboard.state.RIGHT then 62 | self.pos.x = util.clamp(self.pos.x + 1, (-#self.lines[self.pos.y] + 1), 1) 63 | elseif keyboard.state.DOWN then 64 | self.pos.y = util.clamp(self.pos.y + 1, 1, #self.lines) 65 | elseif keyboard.state.BACKSPACE then 66 | self:rm(false) 67 | elseif keyboard.state.DELETE then 68 | self:rm(true) 69 | elseif keyboard.state.ENTER then 70 | if not keyboard.shift() then 71 | self.pos.y = util.clamp(self.pos.y + 1, 1, 7) 72 | end 73 | end 74 | if #self.lines[self.pos.y] < 1 then self.pos.x = 1 end 75 | end 76 | 77 | 78 | textedit.kb_char = function(self, k) 79 | self:buildword(k) 80 | end 81 | 82 | textedit.render = function(blink, run, output) 83 | screen.font_face(25) 84 | screen.font_size(6) 85 | 86 | screen.level(run % 2 == 0 and 6 or 1) 87 | screen.rect(124, 60, 4, 4) 88 | screen.fill() 89 | 90 | if textedit.evaluated then 91 | screen.level(2) 92 | screen.rect(0, 0, 128, 57) 93 | screen.fill() 94 | textedit.evaluated = false 95 | end 96 | 97 | screen.level(15) 98 | 99 | for i = 1, #textedit.lines do 100 | screen.move(0, 8 * i) 101 | local l = table.concat(textedit.lines[i]) 102 | screen.text(tostring(l)) 103 | screen.stroke() 104 | end 105 | 106 | screen.level(3) 107 | screen.move(0, 63) 108 | screen.text(output[#output]) 109 | screen.stroke() 110 | 111 | if blink then 112 | screen.level(2) 113 | screen.rect((((#textedit.lines[textedit.pos.y] + textedit.pos.x) * 8) / 2) - 4, (textedit.pos.y * 8) - 6, 3, 7) 114 | screen.fill() 115 | end 116 | end 117 | 118 | return textedit 119 | -------------------------------------------------------------------------------- /lib/keycodes.lua: -------------------------------------------------------------------------------- 1 | -- keyboard map class 2 | -- @okyeron 3 | local keycodes = {} 4 | 5 | keycodes.keys = { 6 | [hid.codes.KEY_1]="1", 7 | [hid.codes.KEY_2]="2", 8 | [hid.codes.KEY_3]="3", 9 | [hid.codes.KEY_4]="4", 10 | [hid.codes.KEY_5]="5", 11 | [hid.codes.KEY_6]="6", 12 | [hid.codes.KEY_7]="7", 13 | [hid.codes.KEY_8]="8", 14 | [hid.codes.KEY_9]="9", 15 | [hid.codes.KEY_0]="0", 16 | [hid.codes.KEY_Q]="Q", 17 | [hid.codes.KEY_W]="W", 18 | [hid.codes.KEY_E]="E", 19 | [hid.codes.KEY_R]="R", 20 | [hid.codes.KEY_T]="T", 21 | [hid.codes.KEY_Y]="Y", 22 | [hid.codes.KEY_U]="U", 23 | [hid.codes.KEY_I]="I", 24 | [hid.codes.KEY_O]="O", 25 | [hid.codes.KEY_P]="P", 26 | [hid.codes.KEY_A]="A", 27 | [hid.codes.KEY_S]="S", 28 | [hid.codes.KEY_D]="D", 29 | [hid.codes.KEY_F]="F", 30 | [hid.codes.KEY_G]="G", 31 | [hid.codes.KEY_H]="H", 32 | [hid.codes.KEY_J]="J", 33 | [hid.codes.KEY_K]="K", 34 | [hid.codes.KEY_L]="L", 35 | [hid.codes.KEY_Z]="Z", 36 | [hid.codes.KEY_X]="X", 37 | [hid.codes.KEY_C]="C", 38 | [hid.codes.KEY_V]="V", 39 | [hid.codes.KEY_B]="B", 40 | [hid.codes.KEY_N]="N", 41 | [hid.codes.KEY_M]="M", 42 | 43 | [hid.codes.KEY_MINUS]="-", 44 | [hid.codes.KEY_EQUAL]="=", 45 | [hid.codes.KEY_APOSTROPHE]="'", 46 | [hid.codes.KEY_GRAVE]="`", 47 | [hid.codes.KEY_COMMA]=",", 48 | [hid.codes.KEY_DOT]=".", 49 | [hid.codes.KEY_SEMICOLON]=";", 50 | [hid.codes.KEY_SLASH]="/", 51 | [hid.codes.KEY_BACKSLASH]="\\", 52 | [hid.codes.KEY_LEFTBRACE]="[", 53 | [hid.codes.KEY_RIGHTBRACE]="]", 54 | [hid.codes.KEY_SPACE]=" ", 55 | [hid.codes.KEY_KPASTERISK]="*", 56 | 57 | [hid.codes.KEY_KPMINUS]="-", 58 | [hid.codes.KEY_KPPLUS]="+", 59 | [hid.codes.KEY_KPDOT]=".", 60 | [hid.codes.KEY_KPEQUAL]="]=", 61 | [hid.codes.KEY_KP0]="0", 62 | [hid.codes.KEY_KP1]="1", 63 | [hid.codes.KEY_KP2]="2", 64 | [hid.codes.KEY_KP3]="3", 65 | [hid.codes.KEY_KP4]="4", 66 | [hid.codes.KEY_KP5]="5", 67 | [hid.codes.KEY_KP6]="6", 68 | [hid.codes.KEY_KP7]="7", 69 | [hid.codes.KEY_KP8]="8", 70 | [hid.codes.KEY_KP9]="9", 71 | [hid.codes.KEY_KPENTER]="Enter", 72 | [hid.codes.KEY_KPSLASH]="Slash", 73 | [hid.codes.KEY_102ND]="102ND", 74 | [hid.codes.KEY_TAB]=" ", 75 | 76 | } 77 | keycodes.shifts = { 78 | [hid.codes.KEY_1]="!", 79 | [hid.codes.KEY_2]="@", 80 | [hid.codes.KEY_3]="#", 81 | [hid.codes.KEY_4]="$", 82 | [hid.codes.KEY_5]="%", 83 | [hid.codes.KEY_6]="^", 84 | [hid.codes.KEY_7]="&", 85 | [hid.codes.KEY_8]="*", 86 | [hid.codes.KEY_9]="(", 87 | [hid.codes.KEY_0]=")", 88 | [hid.codes.KEY_LEFTBRACE]="{", 89 | [hid.codes.KEY_RIGHTBRACE]="}", 90 | [hid.codes.KEY_COMMA]="<", 91 | [hid.codes.KEY_DOT]=">", 92 | [hid.codes.KEY_SLASH]="?", 93 | [hid.codes.KEY_SEMICOLON]=":", 94 | [hid.codes.KEY_APOSTROPHE]="\"", 95 | [hid.codes.KEY_BACKSLASH]="|", 96 | [hid.codes.KEY_MINUS]="_", 97 | [hid.codes.KEY_EQUAL]="+", 98 | [hid.codes.KEY_GRAVE]="~", 99 | } 100 | 101 | keycodes.cmds = { 102 | [hid.codes.KEY_ESC]="ESC", 103 | [hid.codes.KEY_LEFTSHIFT]="Left Shift", 104 | [hid.codes.KEY_RIGHTSHIFT]="Right Shift", 105 | [hid.codes.KEY_LEFTALT]="Left Alt", 106 | [hid.codes.KEY_RIGHTALT]="Right Alt", 107 | [hid.codes.KEY_LEFTCTRL]="Left CTRL", 108 | [hid.codes.KEY_RIGHTCTRL]="Right CTRL", 109 | [hid.codes.KEY_BACKSPACE]="Backspace", 110 | [hid.codes.KEY_DELETE]="Delete", 111 | [hid.codes.KEY_ENTER]="Enter", 112 | [hid.codes.KEY_CAPSLOCK]="Capslock", 113 | [hid.codes.KEY_NUMLOCK]="Numlock", 114 | [hid.codes.KEY_SCROLLLOCK]="Scroll Lock", 115 | 116 | [hid.codes.KEY_SYSRQ]="SYSRQ", 117 | [hid.codes.KEY_HOME]="Home", 118 | [hid.codes.KEY_UP]="Up", 119 | [hid.codes.KEY_PAGEUP]="Pageup", 120 | [hid.codes.KEY_LEFT]="Left", 121 | [hid.codes.KEY_RIGHT]="Right", 122 | [hid.codes.KEY_END]="End", 123 | [hid.codes.KEY_DOWN]="Down", 124 | [hid.codes.KEY_PAGEDOWN]="Page Down", 125 | [hid.codes.KEY_INSERT]="Insert", 126 | [hid.codes.KEY_PAUSE]="Pause", 127 | [hid.codes.KEY_LEFTMETA]="Left Meta", 128 | [hid.codes.KEY_RIGHTMETA]="Right Meta", 129 | [hid.codes.KEY_COMPOSE]="Compose" 130 | } 131 | 132 | 133 | return keycodes 134 | -------------------------------------------------------------------------------- /lib/repl.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- repl module 3 | -- @its_your_bedtime 4 | -- 5 | 6 | 7 | local repl = { 8 | lines = {{},{},{},{},{},{}, {},{}}, 9 | history = { entries = { [0] = '' }, index = 0 }, 10 | offset = { x = 0, y = 0 }, 11 | pos = {x = 1, y = 1}, 12 | output = '', 13 | evaluated = false, 14 | running = false, 15 | view = 10, 16 | nl = 0 17 | } 18 | 19 | repl.hist = function(self, i) 20 | local e, t = self.history.entries[i], {} 21 | for i in e:gmatch('.') do t[#t + 1] = i end 22 | return t 23 | end 24 | 25 | repl.upd_hist = function(self, d) 26 | self.history.index = util.clamp(self.history.index + d, 0, #self.history.entries ) 27 | self.lines[1] = self:hist(self.history.index) 28 | end 29 | 30 | repl.buildword = function(self, keyinput) 31 | if keyinput ~= nil then 32 | if self.pos.x ~= 1 then 33 | table.insert(self.lines[self.pos.y] , (#self.lines[self.pos.y] + self.pos.x) , keyinput) 34 | else 35 | table.insert(self.lines[self.pos.y], keyinput) 36 | end 37 | 38 | if #self.lines[self.pos.y] >= 31 then 39 | self.view = util.clamp(self.view - 1, 4, 10) 40 | self.pos.y = util.clamp(self.pos.y + 1, 1, 7) 41 | self.pos.x = 1 42 | end 43 | end 44 | end 45 | 46 | repl.rm = function(self, back) 47 | if back then 48 | if (#self.lines[self.pos.y] + self.pos.x) <= #self.lines[self.pos.y] then 49 | 50 | table.remove(self.lines[self.pos.y], util.clamp((#self.lines[self.pos.y] + self.pos.x), 0, #self.lines[self.pos.y])) 51 | self.pos.x = self.pos.x + 1 52 | end 53 | else 54 | if (#self.lines[self.pos.y] + self.pos.x) > 1 then 55 | local pos = util.clamp((#self.lines[self.pos.y] + self.pos.x) - 1, 0, #self.lines[self.pos.y]) 56 | table.remove(self.lines[self.pos.y], pos) 57 | end 58 | if #self.lines[self.pos.y] + self.pos.x == 1 then 59 | self.pos.y = util.clamp(self.pos.y - 1, 1, #self.lines) 60 | self.view = util.clamp(self.view + 1, 1, 10) 61 | end 62 | end 63 | end 64 | 65 | repl.kb = function(self) 66 | if keyboard.state.UP then 67 | if self.nl == 0 then 68 | self:upd_hist(1) 69 | else 70 | self.pos.y = util.clamp(self.pos.y - 1, 1, #self.lines) 71 | self.pos.x = 1 72 | end 73 | elseif keyboard.state.LEFT then 74 | self.pos.x = util.clamp(self.pos.x - 1, ((-#self.lines[self.pos.y])+ 1) , 1) 75 | elseif keyboard.state.RIGHT then 76 | self.pos.x = util.clamp(self.pos.x + 1,( -#self.lines[self.pos.y]), 1) 77 | elseif keyboard.state.DOWN then 78 | if self.nl == 0 then 79 | self:upd_hist(-1) 80 | else 81 | self.pos.y = util.clamp(self.pos.y + 1, 1, #self.lines ) 82 | self.pos.x = 1 83 | end 84 | elseif keyboard.state.BACKSPACE then 85 | self:rm(false) 86 | elseif keyboard.state.DELETE then 87 | self:rm(true) 88 | elseif keyboard.state.ENTER then 89 | self.pos.y = util.clamp(self.pos.y + 1, 1, 7) 90 | self.pos.x = #self.lines[self.pos.y + 1] > 0 and #self.lines[self.pos.y] or 1 91 | end 92 | end 93 | 94 | repl.evaluate = function(self) 95 | 96 | local f = '' 97 | for i = 1, #repl.lines do 98 | local l = table.concat(repl.lines[i]) 99 | f = f .. tostring(l) 100 | end 101 | 102 | repl.nl = 0 103 | 104 | for c in f:gmatch('[()]') do 105 | repl.nl = util.clamp(repl.nl + (c == '(' and 1 or -1), 0, 8) 106 | end 107 | table.insert(repl.lines[repl.pos.y],'\n') 108 | repl.view = util.clamp(repl.view - repl.nl, 3, 10) 109 | 110 | if repl.nl == 0 then 111 | repl.view = 10 112 | repl.pos.y,repl.pos.x = 1, 1 113 | if string.len(f:gsub("%s+", "")) > 0 then 114 | repl.lines = {{},{},{},{},{},{},{},{}} 115 | table.insert( repl.history.entries, 1, f ) 116 | self.run(f, true) 117 | repl.history.index = 0 118 | end 119 | end 120 | end 121 | 122 | repl.render = function(self, blink) 123 | local line = 1 124 | 125 | if repl.running then 126 | screen.level(3) 127 | screen.rect(124, 60, 4, 4) 128 | screen.fill() 129 | repl.running = false 130 | end 131 | 132 | screen.font_face(25) 133 | screen.font_size(6) 134 | screen.level(15) 135 | 136 | for i = 1, #repl.lines do 137 | local rev = #repl.lines - i 138 | local l = table.concat(repl.lines[i]) 139 | screen.move(0, (48 + (((repl.view - 9) + i) *7 ))) 140 | screen.text((i == 1 and '>' or '') .. tostring(l)) 141 | screen.stroke() 142 | end 143 | 144 | screen.level(3) 145 | 146 | for i = #self.output - 8, #self.output do 147 | screen.move(0, (62 + (((line + repl.view) - 20) * 7))) 148 | screen.text(tostring(self.output[i - repl.offset.x] or '')) 149 | line = line + 1 150 | end 151 | 152 | screen.stroke() 153 | 154 | if blink then 155 | local strlen = #repl.lines[repl.pos.y] + repl.pos.x 156 | local intend = ((strlen * 8) / 2) - (repl.pos.y == 1 and 0 or 4) 157 | screen.level(2) 158 | screen.rect(intend, (42 + (((repl.view - 9) + (repl.pos.y))*7)), 3, 7) 159 | screen.fill() 160 | end 161 | end 162 | 163 | 164 | return repl 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NISP 2 | 3 | *Scheme dialect livecoding tracker for norns* 4 | 5 | **Work in progress. Everything is subject to change** 6 | 7 | 8 | ### Controls 9 | 10 | **`~`**|`open / close repl` 11 | 12 | **`shift + ctrl`**|`start / stop playback` 13 | 14 | **`capslock`**|`toggle follow mode` 15 | 16 | **`shift + 1 - 4`**|`mute track` 17 | 18 | **`enter`**|`open text editor (third column of track must be selected)` 19 | 20 | **`esc`**|`close text editor` 21 | 22 | **`shift + enter`**|`save expression in cell (while in text edit mode)` 23 | 24 | **`ctrl + c / v`**|`copy / paste` 25 | 26 | **`shift + esc`**|`open / close norns menu` 27 | 28 | **`ctrl + left / right`**|`switch menu tabs` 29 | 30 | **`shift + up / down`**|`fast nav between samples in params menu` 31 | 32 | 33 | ### Commands 34 | 35 | | function | arguments | description 36 | |:-------------|:------------------------|:----------------------------------------------------------- 37 | | `(def)` | `symbol, value` | define a symbol | 38 | | `(lambda)` | `args, function` | anonymous function | 39 | | `(quote)` `'`| `expr` | returns unevaluated expression | 40 | | `(if)` | `cond, expr 1, expr 2` | evaluate expr 1 if cond is true, else evaluates expr 2 | 41 | | `(when)` | `cond, expr` | evaluate expr if cond is true | 42 | | `(@)` | `track` *optional*| returns current position | 43 | | `(bpm)` | `value` | set global bpm | 44 | | `(length)` | `value` | set pattern length | 45 | | `(div)` | `value` | set track speed divider | 46 | | `(jmp)` | `pos` | jump to position | 47 | | `(skip)` | `pos` | skip current step | 48 | | `(ever)` | `N, expr` | evaluate expression every **N** cycle | 49 | | `(mute)` | `track` *optional* | mute track | 50 | | `(sync)` | `track` *optional* | sync positions to track, or to global pos | 51 | | `(save)` | `id` | save pattern | 52 | | `(load)` | `id` | load pattern | 53 | | `(note)` | `value` | write note at current position | 54 | | `(sample)` | `value` | write sample at current position | 55 | | `(pos)` | `value` | set position of current sample | 56 | | `(param)` | `value` | set current sample [param](#extras) | 57 | | `(help)` | - | display help | 58 | 59 | ###### Math 60 | `(+)` `(-)` `(*)` `(/)` `(%)` `(^)` `(=)` `(eq)` `(>)` `(<)` `(<=)` `(>=)` `(rnd)` 61 | ###### Other 62 | `(list)` `(list?)` `(append)` `(apply)` `(begin)` `(car)` `(cdr)` `(cons)` 63 | `(len)` `(get)` `(put)` `(nil?)` `(num?)` `(print)` `(concat)` `(map)` `(#t)` `(#f)` 64 | 65 | 66 | ##### Examples 67 | *Functions can be executed either live in repl or from pattern cells.* 68 | 69 | ```common-lisp 70 | 71 | (def A 1) - sets symbol A to 1 72 | 73 | (print "Hello") - double quotes for strings 74 | 75 | (def A (lambda () (print "Hello"))) - defines a function (A) with no arguments 76 | 77 | (def A (lambda (a b c) (+ a b c))) - defines a function (A) which takes 3 arguments and returns their sum. 78 | 79 | (get '(1 2 3 4) 1) - returns first element from list 80 | 81 | (bpm 120) - set bpm to 120 82 | 83 | (jmp) - set current track position to the very beginning 84 | 85 | (pos (rnd 1 99)) - set random start position for current sample 86 | 87 | (atk 0.25) - set current sample attack to 0.25 88 | ``` 89 |
90 | 91 | ###### Extras 92 | [more about scheme](http://www.shido.info/lisp/idx_scm_e.html) 93 | 94 |
95 | params shortenings 96 | 97 | atk - amp_env_attack 98 | dec - amp_env_decay 99 | sus - amp_env_sustain 100 | rel - amp_env_release 101 | detune - detune_cents 102 | strtch - by_percentage 103 | ctf - filter_freq 104 | res - filter_resonance 105 | ftype - filter_type 106 | qlt - quality 107 | fm-lfo1 - freq_mod_lfo_1 108 | fm-lfo2 - freq_mod_lfo_2 109 | f-lfo1 - filter_freq_mod_lfo_1 110 | f-lfo2 - filter_freq_mod_lfo_2 111 | p-lfo1 - pan_mod_lfo_1 112 | p-lfo2 - pan_mod_lfo_2 113 | a-lfo1 - amp_mod_lfo_1 114 | a-lfo2 - amp_mod_lfo_2 115 | fm-env - freq_mod_env 116 | f-fm-env - filter_freq_mod_env 117 | f-fm-vel - filter_freq_mod_vel 118 | f-fm-pr - filter_freq_mod_pressure 119 | f-track - filter_tracking 120 | p-env - pan_mod_env 121 | m-atk - mod_env_attack 122 | m-dec - mod_env_decay 123 | m-sus - mod_env_sustain 124 | m-rel - mod_env_release 125 | 126 |
127 | 128 | 129 | 130 |
131 | 132 | 133 | ###### Known bugs-features: 134 | 135 | copy-pasted expression cells are linked, so editing one would affect all others. 136 | -------------------------------------------------------------------------------- /lib/_corelib.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- nisp core functions 3 | --- @its_your_bedtime 4 | --- 5 | 6 | local shortenings = { ["atk"] = "amp_env_attack", ["dec"] = "amp_env_decay", ["sus"] = "amp_env_sustain", 7 | ["rel"] = "amp_env_release", ["detune"] = "detune_cents", ["strch"] = "by_percentage", 8 | ["ctf"] = "filter_freq", ["res"] = "filter_resonance", ["ftype"] = "filter_type", 9 | ["qlt"] = "quality", ["fm-lfo1"] = "freq_mod_lfo_1", ["fm-lfo2"] = "freq_mod_lfo_2", 10 | ["f-lfo1"] = "filter_freq_mod_lfo_1", ["f-lfo2"] = "filter_freq_mod_lfo_2", 11 | ["p-lfo1"] = "pan_mod_lfo_1", ["p-lfo2"] = "pan_mod_lfo_2", ["a-lfo1"] = "amp_mod_lfo_1", 12 | ["a-lfo2"] = "amp_mod_lfo_2", ["fm-env"] = "freq_mod_env", ["f-fm-env"] = "filter_freq_mod_env", 13 | ["f-fm-vel"] = "filter_freq_mod_vel", ["f-fm-pr"] = "filter_freq_mod_pressure", 14 | ["f-track"] = "filter_tracking", ["p-env"] = "pan_mod_env", ["m-atk"] = "mod_env_attack", 15 | ["m-dec"] = "mod_env_decay", ["m-sus"] = "mod_env_sustain", ["m-rel"] = "mod_env_release" 16 | } 17 | 18 | local core = { 19 | 20 | ------ 21 | ------ 22 | ------ 23 | ['help'] = function( self ) return self.help end, 24 | ['quote'] = function( self, x, env ) return x[2] end, 25 | ['def'] = function( self, x, env ) env[x[2]] = self.eval(x[3], env) end, 26 | ['lambda'] = function( self, x, env ) return self.Proc(x[2], x[3], env) end, 27 | ['@'] = function( self, x, env ) return self.pos_now end, 28 | ['bpm'] = function( self, x, env ) self.bpm = self.eval(x[2], env) self.metro:bpm_change(self.bpm) end, 29 | ['div'] = function(self, x, env) local div = tonumber(self.eval(x[2], env)) self.div[self.tr_now] = div or 1 end, 30 | 31 | ------ 32 | ------ 33 | ------ 34 | ['if'] = function( self, x, env ) 35 | local exp = self.eval(x[2], env) 36 | if exp == true then return self.eval(x[3], env) 37 | elseif exp == false or exp == nil then 38 | return self.eval(x[4], env ) 39 | end 40 | end, 41 | 42 | ['when'] = function( self, x, env ) 43 | local exp = self.eval(x[2], env) 44 | if exp == true then return self.eval(x[3], env) 45 | end 46 | end, 47 | 48 | ['jmp'] = function( self, x, env ) 49 | local l = util.clamp(self.eval(x[2], env) or 1 - 1, 0, self.length) 50 | self.subpos[self.tr_now] = l or 0 51 | end, 52 | 53 | ['skip'] = function( self, x, env ) 54 | self.subpos[self.tr_now] = self.subpos[self.tr_now] + 1 55 | end, 56 | 57 | ['mute'] = function( self, x, env ) 58 | if #x == 2 then 59 | local dst = self.eval(x[2], env) 60 | self.mute[tonumber(dst)] = not self.mute[tonumber(dst)] 61 | else 62 | self.mute[self.tr_now] = not self.mute[self.tr_now] 63 | end 64 | end, 65 | 66 | ['sync'] = function( self, x, env ) 67 | if #x == 2 then 68 | local sync_to = self.subpos[util.clamp(self.eval(x[2], env), 1, 4)] 69 | for i = 1, 4 do self.subpos[i] = sync_to end 70 | else 71 | for i = 1, 4 do self.subpos[i] = self.pos end 72 | end 73 | end, 74 | 75 | ['ever'] = function( self, x, env ) 76 | local s = self.eval(x[2], env) 77 | if self.cycle[self.tr_now] % s == 0 then 78 | return self.eval(x[3], env) 79 | end 80 | end, 81 | 82 | ['length'] = function( self, x, env ) 83 | local length = self.eval(x[2], env) or 16 84 | self.length = util.clamp(length, self.pos_now or 1, 99) 85 | end, 86 | 87 | 88 | ['save'] = function( self, x, env ) 89 | local data = { pat = self.pat, bpm = self.bpm, div = self.div, length = self.length, mute = self.mute } 90 | tab.save( { nil , data }, norns.state.data .. tostring(self.eval(x[2], env)) ..".seq") 91 | end, 92 | 93 | ['load'] = function( self, x, env ) 94 | local saved = tab.load(norns.state.data .. tostring(self.eval(x[2], env)) .. ".seq") 95 | if saved ~= nil then for k,v in pairs(saved[2]) do self[k] = v end end 96 | self.metro:bpm_change(saved[2].bpm) 97 | end, 98 | 99 | -------------------------------- 100 | ---------SOUND-OPS-------------- 101 | -------------------------------- 102 | ['note'] = function( self, x, env ) 103 | if #x == 3 then 104 | local step = self.eval(x[2], env) 105 | self.pat[step][self.tr_now * 3 - 1] = self.eval(x[3], env) 106 | else 107 | self.pat[self.pos_now][self.tr_now * 3 - 1] = util.clamp(self.eval(x[2], env), 0, 99) 108 | end 109 | end, 110 | 111 | 112 | ['sample'] = function( self, x, env ) 113 | if #x == 3 then 114 | local step = self.eval(x[2], env) 115 | self.pat[step][self.tr_now * 3 - 2] = self.eval(x[3], env) 116 | else 117 | self.pat[self.pos_now][self.tr_now * 3 - 2] = util.clamp(self.eval(x[2], env), 0, 99) 118 | end 119 | end, 120 | 121 | 122 | ['pos'] = function( self, x, env ) 123 | local s_id = tonumber(self.pat[self.pos_now][self.tr_now * 3 - 2]) or false 124 | if not s_id then return false end 125 | local length = params:get("end_frame_" .. s_id) 126 | local val = self.eval(x[2]) 127 | local start_pos = util.clamp((tonumber(val) / 100 ) * length, 0, length ) 128 | params:set("start_frame_" .. s_id, start_pos or 0) 129 | end, 130 | 131 | } 132 | ------ 133 | ------ 134 | ------ 135 | 136 | for k, v in pairs(shortenings) do 137 | core[k] = function(self, x, env) 138 | local s_id = tonumber(self.pat[self.pos_now][self.tr_now * 3 - 2]) or false 139 | if not s_id then return false end 140 | local val = self.eval(x[2], env) 141 | if val then params:set( shortenings[k] .. "_" .. s_id , val ) end 142 | end 143 | end 144 | 145 | 146 | 147 | return core -------------------------------------------------------------------------------- /lib/lisp.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- lisp core 3 | -- @its_your_bedtime 4 | -- 5 | 6 | local keyboard = require 'core/keyboard' 7 | local stdlib = include('lib/_stdlib') 8 | local utils = include('lib/utils') 9 | local repl = include('lib/repl') 10 | local engines = include('lib/_engines') 11 | local tracker = include('lib/tracker') 12 | local beatclock = require('beatclock') 13 | 14 | local lisp = { 15 | output = {''}, metro = beatclock.new(), 16 | core = include('lib/_corelib'), std = nil, log = utils.log, 17 | buf = {}, blink = false, bpm = 110, live = false, 18 | tracker = true, pat = {[0] = {}}, help = '', 19 | pos = 1, subpos = { 1, 1, 1, 1 }, length = 16, 20 | mute ={ false, false, false, false }, 21 | cycle = { 1, 1, 1, 1 }, div = { 1, 1, 1, 1 }, 22 | tr_now = 1, pos_now = 1, 23 | } 24 | 25 | local is_mute_shortcut = false 26 | function lisp.kb_code(c, val) 27 | is_mute_shortcut = false 28 | if lisp.live then repl:kb() 29 | elseif lisp.tracker then 30 | tracker.kb_code(c, val, lisp.pat, lisp.length) 31 | local code = utils.tab_key(keyboard.codes, c) 32 | if val > 0 and keyboard.shift() and (code <= 5 and code >= 2) then 33 | is_mute_shortcut = true 34 | lisp.mute[code - 1] = not lisp.mute[code - 1] 35 | end 36 | end 37 | if keyboard.state.ENTER then 38 | if lisp.live then 39 | repl.evaluate(lisp) 40 | end 41 | end 42 | if keyboard.shift() and keyboard.ctrl() then 43 | if lisp.metro.playing then 44 | lisp.metro:stop() lisp:log('> stopped.') 45 | else lisp.metro:start() lisp:log('> running..') end 46 | end 47 | end 48 | 49 | function lisp.kb_char(k) 50 | if lisp.live then repl:buildword(k) 51 | elseif lisp.tracker then 52 | tracker.kb_char(k) 53 | if keyboard.ctrl() then 54 | if k == 'x' then 55 | tracker:copy(lisp.pat[tracker.pos.y][tracker.pos.x]) 56 | lisp.pat[tracker.pos.y][tracker.pos.x] = nil 57 | elseif k == 'c' then 58 | tracker:copy(lisp.pat[tracker.pos.y][tracker.pos.x]) 59 | elseif k == 'v' then 60 | lisp.pat[tracker.pos.y][tracker.pos.x] = tracker:paste() 61 | end 62 | elseif is_mute_shortcut then 63 | -- pass 64 | else 65 | tracker:buildword(k, lisp.pat) 66 | end 67 | end 68 | end 69 | 70 | lisp.enc = function(self, n, d) 71 | if self.live then repl:enc(n, d, lisp) else end 72 | end 73 | 74 | lisp.init = function() 75 | local blinks = metro.init( function() lisp.blink = not lisp.blink end, 0.7) 76 | blinks:start() 77 | lisp.metro:add_clock_params() 78 | lisp.metro.on_step = function() tracker.exec(lisp) end 79 | engines.init() 80 | -- init environment 81 | lisp.std = lisp.Env({}, {}, {_find_= function() return nil end }) -- outer table doesn't exists 82 | for k,_ in pairs(lisp.core) do lisp.help = lisp.help ..' '.. k end 83 | for k,v in pairs(stdlib) do lisp.std[k] = v lisp.help = lisp.help ..' '.. k end -- functions 84 | for k = 1, 99 do lisp.pat[k] = {} end 85 | 86 | lisp:log('welcome to nisp') 87 | end 88 | 89 | ---------- 90 | 91 | -- Prepare table for environment 92 | lisp.Env = function (pars, args, outer) 93 | local dict = { outer = outer } 94 | for i = 1, #pars do dict[pars[i]] = args[i] end 95 | -- check existance of symbol 96 | dict._find_ = function (self, v) return self[v] and self or self.outer:_find_(v) end 97 | return dict 98 | end 99 | 100 | lisp.collect = function(t,s,e) 101 | local args = {} 102 | for i = s, #t do 103 | local v = lisp.eval(t[i], e ) 104 | if v then 105 | args[#args + 1] = v 106 | end 107 | end 108 | return args 109 | end 110 | 111 | -- Expression evaluation 112 | lisp.eval = function (x, env) 113 | env = env or lisp.std 114 | if type(x) ~= 'table' then 115 | local elt = env:_find_(x) 116 | if elt then return elt[x] end -- is symbol 117 | if tonumber(x) then return tonumber(x) end -- is number 118 | if x == nil then return false end 119 | if string.find(x, '".*"') then return x end -- is string ("" must be used) 120 | lisp:log('Undefined: ' .. utils.tostring(x)) 121 | --lisp.err('unknown: ' .. utils.tostring(x)) 122 | return nil 123 | else 124 | if lisp.core[x[1]] then 125 | return lisp.core[x[1]](lisp, x, env) 126 | elseif env:_find_(x[1]) then 127 | local proc = lisp.eval(x[1], env) 128 | if type(proc) == 'function' then 129 | local args = lisp.collect(x, 2, env) 130 | return proc(table.unpack(args)) or nil 131 | else 132 | local args = lisp.collect(x, 2, env) 133 | return proc(table.unpack(args)) or nil 134 | end 135 | 136 | else 137 | local args = lisp.collect(x, 1, env) 138 | return args 139 | end 140 | end 141 | 142 | end 143 | 144 | -- User defined procedure (lambda) 145 | lisp.Proc = function (pars, body, env) 146 | local dict = {pars = pars, body = body, env = env} 147 | -- make callable 148 | setmetatable(dict, { __call = function (self, ...) 149 | return lisp.eval(self.body, lisp.Env(self.pars, {...}, self.env)) 150 | end }) 151 | return dict 152 | end 153 | 154 | -- Create list (parsing tree) from tokens 155 | lisp.tree = function (tok) 156 | local t = table.remove(tok, 1) 157 | if t == '\'' then return {'quote', lisp.tree(tok)} end -- use ' for quote 158 | if t == '(' then -- new list 159 | local tbl = {} 160 | while tok[1] ~= ')' do 161 | tbl[#tbl + 1] = lisp.tree(tok) 162 | end 163 | table.remove(tok, 1) 164 | return tbl 165 | else 166 | return t ~= ')' and t or lisp.err("syntax error") 167 | end 168 | end 169 | 170 | -- Get tokens 171 | lisp.parse = function (s) 172 | s = s:gsub(';+.-\n', '\n') -- remove comments 173 | s = s:gsub('[()\']', {['(']=' ( ',[')']=' ) ', ['\'']=' \' '}) -- white space as delimeter 174 | local quotes = false 175 | local tok = {} 176 | 177 | for w in s:gmatch('%S+') do 178 | if(quotes) then 179 | tok[#tok] = tok[#tok] .. ' ' .. w 180 | else 181 | tok[#tok + 1] = w 182 | end 183 | 184 | if(utils.startswith(w, '"')) then 185 | quotes = true 186 | end 187 | 188 | if(utils.endswith(w, '"')) then 189 | quotes = false 190 | end 191 | end 192 | 193 | return lisp.tree(tok) 194 | end 195 | 196 | 197 | lisp.run = function(str, verbose, tr, pos) 198 | lisp.tr_now = tr 199 | lisp.pos_now = pos 200 | if verbose then lisp:log('>'..str) end 201 | local res = (#str > 0) and lisp.eval(lisp.parse(str)) or nil 202 | if res then lisp:log(res) end 203 | if not verbose then return res end 204 | end 205 | 206 | lisp.err = function(msg) 207 | lisp:log(msg) 208 | error(msg) 209 | end 210 | 211 | lisp.redraw = function() 212 | if lisp.live then repl.render(lisp, lisp.blink) 213 | elseif lisp.tracker then tracker.render(lisp) 214 | end 215 | 216 | end 217 | return lisp 218 | -------------------------------------------------------------------------------- /lib/tracker.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tracker module 3 | -- @its_your_bedtime 4 | -- 5 | 6 | local music = require 'musicutil' 7 | --- 8 | local textedit = include('lib/textedit') 9 | local utils = include('lib/utils') 10 | --- 11 | local tr_i = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12} } 12 | local tracker = { pos = { x = 1, y = 1}, edit = false, buffer = { nil, nil }, } 13 | local bars = { [1] = { 11, 28, 31 }, [2] = { 39, 31, 61 }, [3] = { 69, 31, 91 }, [4] = { 99, 30, 121 } } 14 | local s_offset, bounds_y, w, attached = 0, 9, {}, false 15 | --- 16 | local function tr_spacing(tr) return (30 * tr) - 17 end 17 | local function cursor_pos(x, y) return (y == tracker.pos.y and tracker.pos.x == x) and 9 or 1 end 18 | local function format_val(v) return (v and string.len(v) < 2) and v .. '-' or v and v or '--' end 19 | local function not_empty(t) for i=1,#t do if #t[i] > 0 then return true end end end 20 | 21 | local function get_note(n) 22 | if n and string.match(n, '%a%d') then 23 | return utils.note_name_to_num(n) 24 | elseif n and string.match(n, '%d%d') then 25 | return tonumber(n) 26 | end 27 | end 28 | 29 | tracker.copy = function(self, p) 30 | if self.pos.x % 3 == 0 then 31 | self.buffer[1] = p 32 | else 33 | self.buffer[2] = p 34 | end 35 | end 36 | 37 | tracker.paste = function(self) 38 | if self.pos.x % 3 == 0 then 39 | return self.buffer[1] 40 | else 41 | return self.buffer[2] 42 | end 43 | end 44 | 45 | tracker.buildword = function(self, keyinput, pat) 46 | if keyinput ~= nil then 47 | if #w <= 1 then 48 | if self.pos.x % 3 ~= 0 then 49 | table.insert(w, keyinput) 50 | local str = table.concat(w) 51 | if string.match(str, '%w') then 52 | pat[self.pos.y][self.pos.x] = str 53 | end 54 | end 55 | end 56 | end 57 | end 58 | 59 | tracker.evaluate = function(self, s, tr, pos) 60 | local f = '(' 61 | for i = 1, #s do 62 | local l = table.concat(s[i]) 63 | f = f .. tostring(l) 64 | end 65 | f = f .. ')' 66 | local l = self.run(f, false, tr, pos) 67 | return l or false 68 | end 69 | 70 | tracker.exec = function(self) 71 | 72 | local pat = self.pat 73 | local pos = self.pos 74 | 75 | for i = 1, 4 do 76 | 77 | if self.pos % (self.div[i] > 0 and self.div[i] or 1) == 0 then 78 | 79 | self.cycle[i] = self.pos >= self.length and self.cycle[i] + 1 or self.cycle[i] 80 | self.subpos[i] = self.subpos[i] >= self.length and 1 or self.subpos[i] + 1 81 | 82 | local tr = tr_i[i] 83 | local step = self.subpos[i] 84 | local s = tonumber(pat[step][tr[1]]) 85 | local n = get_note(pat[step][tr[2]]) 86 | local e, l = pat[step][tr[3]] 87 | 88 | if e then l = tracker.evaluate(self, e, i, step ) end 89 | 90 | if not self.mute[i] and s then 91 | engine.noteOn(s, music.note_num_to_freq(n or 60), 1, s) 92 | end 93 | end 94 | end 95 | 96 | if attached then 97 | 98 | if tracker.pos.y < self.pos then s_offset = self.pos - bounds_y end 99 | 100 | s_offset = utils.update_offset(s_offset, self.pos, self.length, bounds_y, 1) 101 | s_offset = self.pos >= self.length and 0 or s_offset 102 | 103 | end 104 | 105 | self.pos = self.pos >= self.length and 1 or self.pos + 1 106 | 107 | end 108 | 109 | tracker.kb_code = function(c, val, pat, length) 110 | if tracker.edit then 111 | if keyboard.state.ESC then tracker.edit = false end 112 | if keyboard.state.ENTER and keyboard.shift() then 113 | textedit.evaluated = true 114 | pat[tracker.pos.y][tracker.pos.x] = textedit:store() 115 | end 116 | textedit:kb_code(c, val) 117 | else 118 | if keyboard.state.UP then 119 | tracker.pos.y = util.clamp(tracker.pos.y - 1, 1, length) 120 | attached, w = false, {} 121 | elseif keyboard.state.LEFT then 122 | tracker.pos.x = util.clamp(tracker.pos.x - 1, 1, 12) 123 | attached, w = false, {} 124 | elseif keyboard.state.RIGHT then 125 | tracker.pos.x = util.clamp(tracker.pos.x + 1, 1, 12) 126 | attached, w = false, {} 127 | elseif keyboard.state.DOWN then 128 | tracker.pos.y = util.clamp(tracker.pos.y + 1, 1, length) 129 | attached, w = false, {} 130 | elseif keyboard.state.CAPSLOCK then 131 | attached = not attached 132 | elseif keyboard.state.BACKSPACE then 133 | if tracker.pos.x % 3 ~= 0 then 134 | local s = pat[tracker.pos.y][tracker.pos.x] 135 | if s and string.len(s) > 1 then 136 | pat[tracker.pos.y][tracker.pos.x] = string.sub(s, 1, 1) 137 | table.remove(w) 138 | else 139 | pat[tracker.pos.y][tracker.pos.x] = nil w = {} 140 | end 141 | else 142 | pat[tracker.pos.y][tracker.pos.x] = nil 143 | end 144 | elseif keyboard.state.ENTER then 145 | if (not tracker.edit and tracker.pos.x % 3 == 0) then 146 | textedit:open(pat[tracker.pos.y][tracker.pos.x]) 147 | tracker.edit = true 148 | end 149 | end 150 | 151 | end 152 | s_offset = utils.update_offset(s_offset, tracker.pos.y, length, bounds_y, 0) 153 | end 154 | 155 | tracker.kb_char = function(k) 156 | if tracker.edit then 157 | textedit:kb_char(k) 158 | end 159 | end 160 | 161 | tracker.render = function(self) 162 | screen.font_face(25) 163 | screen.font_size(6) 164 | if tracker.edit then 165 | textedit.render(self.blink, self.pos, self.output) 166 | else 167 | screen.level(3) 168 | 169 | for i = 1, 4 do 170 | if not self.mute[i] then 171 | screen.rect(bars[i][1], ((self.subpos[i] - s_offset) * 7) - 6, bars[i][2], 7) 172 | else 173 | screen.rect(bars[i][3], ((self.subpos[i] - s_offset) * 7) - 6, 7, 7) 174 | end 175 | end 176 | 177 | screen.fill() 178 | 179 | for i= 1, bounds_y do 180 | local l = i + s_offset 181 | if l > self.length then return false end 182 | screen.level(4) 183 | screen.move(7, i * 7) 184 | screen.text_right(l) 185 | screen.stroke() 186 | 187 | for k = 1, 4 do 188 | local mute = self.mute[k] 189 | local tr = tr_i[k] 190 | local trk, note, expr = self.pat[l][tr[1]], self.pat[l][tr[2]], self.pat[l][tr[3]] 191 | 192 | screen.level(cursor_pos(tr[1], l)) 193 | screen.move(tr_spacing(k), i * 7) 194 | screen.text(format_val(trk)) 195 | 196 | screen.level(cursor_pos(tr[2], l)) 197 | screen.move(tr_spacing(k) + 10, i * 7) 198 | screen.text(format_val(note)) 199 | 200 | screen.level(cursor_pos(tr[3], l)) 201 | screen.move(tr_spacing(k) + 20, i * 7) 202 | screen.text(not_empty(expr or {}) and '*' or '-') 203 | end 204 | screen.stroke() 205 | end 206 | end 207 | end 208 | 209 | return tracker 210 | --------------------------------------------------------------------------------