Last active
March 16, 2019 20:24
-
-
Save tlubke/a3335a9034830db7ec5e096d54f48fc5 to your computer and use it in GitHub Desktop.
a Polyrhythmic sequencer/sampler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Ekombi | |
-- @tyler | |
-- polyrhythmic sampler | |
-- | |
-- | |
-- 4, two-track channels | |
-- ------------------------------------------ | |
-- trackA: sets the length | |
-- of the tuplet | |
-- | |
-- trackB: sets length of the | |
-- 'measure' in quarter notes | |
-- ------------------------------------------- | |
-- | |
-- works with or without grid | |
-- | |
-- grid controls | |
-- --------------------------------- | |
-- hold a key and press another | |
-- key in the same row to set | |
-- the length of the track | |
-- | |
-- tapping gridkeys toggles the | |
-- tuplet subdivisions and | |
-- quarter notes on/off | |
-- ------------------------------------------- | |
-- | |
-- norns controls | |
-- ------------------------------------------ | |
-- PLAY MODE | |
-- enc1: bpm | |
-- enc2: select pattern | |
-- enc3: filter cutoff | |
-- | |
-- key1: ALT | |
-- ALT+key2: Save pattern | |
-- key2: HOLD->Load pattern | |
-- key3: stop clock | |
-- key3: HOLD->EDIT MODE | |
-- | |
-- EDIT MODE | |
-- enc1: track select | |
-- enc2: subdiv. select | |
-- enc3: length select | |
-- | |
-- key1: ALT | |
-- ALT+key2: Save pattern | |
-- key2: HOLD->Load pattern | |
-- key3: toggle subdiv. on/off | |
-- key3: HOLD->PLAY MODE | |
-- --------------------------------------------- | |
-- | |
-- RANDOM MODE | |
-- --------------------------------------------- | |
-- randomly change a handful | |
-- of parameters on each | |
-- sample triggered. | |
-- | |
-- In the parameter menu... | |
-- | |
-- mode 0: none/off | |
-- mode 1: total random | |
-- mode 2: step-based random | |
-- (like Drunk Obj. in Max/MSP) | |
-- | |
-- each track can be muted from | |
-- so as not to be altered by | |
-- random mode. | |
-- | |
-- 0 = affectable | |
-- 1 = unaffectable | |
engine.name = 'Ack' | |
ack = require 'we/lib/ack' | |
local g = grid.connect() | |
------------ | |
-- variables | |
------------ | |
-- clocking variables | |
local position = 0 | |
local q_position = 0 | |
local counter = nil | |
local running = false | |
local ppq = 480 -- pulses per quarter, lower this if you come across performance issues. | |
-- pattern variables | |
local pattern_select = 1 | |
local pattern_display = "default" | |
-- grid variables | |
-- for holding one gridkey and pressing another further right | |
local g_held = {} | |
local g_heldMax = {} | |
local done = {} | |
local first = {} | |
local second = {} | |
for row = 1,8 do | |
g_held[row] = 0 | |
g_heldMax[row] = 0 | |
done[row] = 0 | |
first[row] = 0 | |
second[row] = 0 | |
end | |
-- 4, two-track channels (A is even rows, TrackB is odd rows) | |
local track = {} | |
for i=1,8 do | |
if i % 2 == 1 then | |
track[i] = {} | |
track[i][1] = {} | |
track[i][1][1] = 0 | |
else | |
track[i] = {} | |
for n=1, 16 do | |
track[i][n] = {} | |
for j=1, 16 do | |
track[i][n][j] = 1 | |
end | |
end | |
end | |
end | |
local mode = 0 -- grid-edit/encoder-edit | |
local alt = false -- alt key | |
---------------- | |
-- initilization | |
---------------- | |
function init() | |
-- parameters | |
params:add_number("bpm", "bpm", 15, 400, 60) | |
params:add_number("random_mode", "random mode:", 0, 2, 0) | |
params:add_number("drunk_step", "mode 2 step size:", 1, 10, 1) | |
params:add_number("1_mute", "mute track 1:", 0, 1, 0) | |
params:add_number("2_mute", "mute track 2:", 0, 1, 0) | |
params:add_number("3_mute", "mute track 3:", 0, 1, 0) | |
params:add_number("4_mute", "mute track 4:", 0, 1, 0) | |
params:set_action("1_mute", function(x) r_mute(1,x) end ) | |
params:set_action("2_mute", function(x) r_mute(2,x) end ) | |
params:set_action("3_mute", function(x) r_mute(3,x) end ) | |
params:set_action("4_mute", function(x) r_mute(4,x) end ) | |
params:add_separator() | |
ack.add_effects_params() | |
params:add_separator() | |
for channel=1,4 do | |
ack.add_channel_params(channel) | |
params:add_separator() | |
end | |
-- | |
params:read("ekombi.pset") | |
-- metronome setup | |
counter = metro.init() | |
counter.time = 60 / (params:get("bpm") * ppq) | |
counter.count = -1 | |
counter.event = count | |
-- | |
blink = 0 | |
blinker = metro.init() | |
blinker.time = 1/11 | |
blinker.count = -1 | |
blinker.event = function(b) | |
blink = blink + 1 | |
redraw() | |
end | |
r_mute("1-4",0) | |
gridredraw() | |
redraw() | |
end | |
local mute = {} | |
function r_mute(track,x) | |
if x == 1 then | |
print("track "..track.." muted") | |
else | |
print("track "..track.." unmuted") | |
end | |
for i=1,4 do | |
if params:get(i.."_mute") == 1 then | |
mute[i] = 1 | |
else | |
mute[i] = 0 | |
end | |
end | |
tab.print(mute) | |
end | |
------------------------- | |
-- grid control functions | |
------------------------- | |
function g.key(x, y, z) | |
-- sending data to two separate functions | |
gridkeyhold(x,y,z) | |
gridkey(x,y,z) | |
print(x,y,z) | |
end | |
function gridkey(x,y,z) | |
if z == 1 then | |
cnt = tab.count(track[y]) | |
-- error control | |
if cnt == 0 or cnt == nil then | |
if x > 1 then | |
return | |
elseif x == 1 then | |
track[y] = {} | |
track[y][x] = {} | |
track[y][x][x] = 1 | |
gridredraw() | |
end | |
return | |
else | |
-- track-B un-reset-able | |
if x == 16 and y % 2 == 1 then | |
track[y] = {} | |
track[y][1] = {} | |
track[y][1][1] = 0 | |
return | |
end | |
-- note toggle on/off | |
if x > cnt then | |
return | |
else | |
if track[y][cnt][x] == 1 then | |
track[y][cnt][x] = 0 | |
else | |
track[y][cnt][x] = 1 | |
end | |
end | |
-- automatic clock startup | |
if running == false then | |
counter:start() | |
running = true | |
end | |
end | |
end | |
redraw() | |
gridredraw() | |
end | |
function gridkeyhold(x, y, z) | |
if z == 1 and g_held[y] then g_heldMax[y] = 0 end | |
g_held[y] = g_held[y] + (z*2 -1) | |
if g_held[y] > g_heldMax[y] then g_heldMax[y] = g_held[y] end | |
if y > 8 and g_held[y] == 1 then | |
first[y] = x | |
elseif y <= 8 and g_held[y] == 2 then | |
second[y] = x | |
elseif z == 0 then | |
if y <= 8 and g_held[y] == 1 and g_heldMax[y] == 2 then | |
track[y] = {} | |
for i = 1, second[y] do | |
track[y][i] = {} | |
for n=1, i do | |
track[y][i][n] = 1 | |
end | |
end | |
end | |
end | |
redraw() | |
gridredraw() | |
end | |
--------------------------- | |
-- norns control functions | |
--------------------------- | |
local track_select = 0 -- 0 indexed, then +1'd later | |
local sub_select = 0 | |
-- length_select is 1 indexed because it is modified in two different places | |
-- in two different ways, one uses the table counting method which itself counts in 1-index | |
local length_select = 1 -- no track-lengths of 0, | |
local cursor = {track_select+1,length_select,sub_select+1} | |
function enc(n,d) | |
if n == 1 then | |
if mode == 0 then | |
params:delta("bpm",d) | |
else | |
track_select = (track_select + d) % 8 | |
length_select = tab.count(track[track_select+1]) | |
print("track "..track_select+1) | |
sub_select = 0 | |
cursor = {track_select+1,length_select,sub_select+1} | |
end | |
end | |
if n == 2 then | |
if mode == 0 then | |
pattern_select = util.clamp(pattern_select + d, 1, 16) | |
print("pattern:"..pattern_select) | |
else | |
sub_select = (sub_select + d) % (length_select) | |
print("sub "..sub_select+1) | |
cursor = {track_select+1,length_select,sub_select+1} | |
end | |
end | |
if n == 3 then | |
if mode == 0 then | |
for i=1, 4 do | |
params:delta(i.."_filter_cutoff", d) | |
end | |
else | |
length_select = ((length_select + d) % 16) | |
if length_select == 0 then length_select = 16 end -- I really didn't want to do this. | |
print("length "..length_select) | |
cursor = {track_select+1,length_select,sub_select+1} | |
track[track_select+1] = {} | |
for i = 1, length_select do | |
track[track_select+1][i] = {} | |
for j=1, i do | |
track[track_select+1][i][j] = 1 | |
end | |
end | |
end | |
end | |
redraw() | |
end | |
function key(n,z) | |
if z == 1 then | |
if n == 1 then | |
alt = true | |
end | |
if n == 2 or n == 3 then | |
if alt == true then | |
save_pattern() | |
key_held = 2*util.time() -- arbitary value to make key_held to nullify release | |
else | |
key_held = util.time() | |
end | |
end | |
else | |
if n == 1 then | |
alt = false | |
end | |
if n == 2 then | |
if key_held - util.time() < -0.333 then -- hold for a third of a second | |
load_pattern() | |
pattern_display = pattern_select | |
end | |
end | |
if n == 3 then | |
if key_held - util.time() < -0.333 then -- hold for a third of a second | |
mode = (mode + 1) % 2 | |
print("mode "..mode) | |
blinker:start() | |
else | |
if mode == 0 then | |
blinker:stop() | |
if running then | |
counter:stop() | |
running = false | |
else | |
position = 0 | |
counter:start() | |
running = true | |
end | |
else | |
track[track_select+1][length_select][sub_select+1] = (track[track_select+1][length_select][sub_select+1] + 1) % 2 | |
end | |
end | |
end | |
end | |
gridredraw() | |
redraw() | |
end | |
------------------ | |
-- active functions | |
------------------- | |
--[[ | |
this is the heart of polyrhythm generating, each track is checked to see which note divisions are on or off, | |
first, the B track is checked (the 'quarter' note, before the tuplet division) then if the note is on, we check | |
each of the subdivisions, and if those turn out to be on, the nth subdivision of the tuple of the track is triggered. | |
The complicated divisons and multiplations of each of the track sets and subsets is to find the exact position value, | |
that when / by that value returns n-1, the track triggers. | |
]]-- | |
function count(c) | |
position = (position + 1) % (ppq) | |
counter.time = 60 / (params:get("bpm") * ppq) | |
if position == 0 then | |
q_position = q_position + 1 | |
fast_gridredraw() | |
end | |
local pending = {} | |
for i=2, 8, 2 do | |
cnt = tab.count(track[i]) | |
if cnt == 0 or cnt == nil then | |
return | |
else | |
if track[i][cnt][(q_position%cnt)+1] == 1 then | |
table.insert(pending,i-1) | |
end | |
end | |
end | |
if tab.count(pending) > 0 then | |
for i=1, tab.count(pending) do | |
cnt = tab.count(track[pending[i]]) | |
if cnt == 0 or cnt == nil then | |
return | |
else | |
for n=1, cnt do | |
if position / ( ppq // (tab.count(track[pending[i]][cnt]))) == n-1 then | |
if track[pending[i]][cnt][n] == 1 then | |
t = (pending[i]//2) + 1 | |
engine.trig(t - 1) -- samples are 0-3 | |
if params:get("random_mode") == 1 then -- mode 1:total random | |
if params:get(t.."_mute") == 0 then | |
params:set(t.."_start_pos", math.random()) | |
--params:set(t.."_speed", math.random()) | |
params:set(t.."_pan", math.random(-1,1)*math.random()) -- -1 or 1 * random float, to fit -1 through 1 panning range | |
params:set(t.."_filter_cutoff", math.random(20,20000)) | |
params:set(t.."_filter_res", math.random()) | |
params:set(t.."_filter_env_atk", math.random()) | |
params:set(t.."_filter_env_rel", math.random()) | |
params:set(t.."_filter_env_mod", math.random()) | |
params:set(t.."_dist", math.random()) | |
end | |
elseif params:get("random_mode") == 2 then -- mode 2: step-based (like drunk from Max) random | |
if params:get(t.."_mute") == 0 then | |
size = params:get("drunk_step") | |
params:delta(t.."_start_pos", (math.random(-10,10)/100)*size) | |
--params:delta(t.."_speed", (math.random(-10,10)/100)*size) | |
params:delta(t.."_pan", (math.random(-10,10)/100)*size) -- -1 or 1 * random float, to fit -1 through 1 panning range | |
params:delta(t.."_filter_cutoff", math.random(-100*size,100*size)) | |
params:delta(t.."_filter_res", (math.random(-10,10)/100)*size) | |
params:delta(t.."_filter_env_atk", (math.random(-10,10)/100)*size) | |
params:delta(t.."_filter_env_rel", (math.random(-10,10)/100)*size) | |
params:delta(t.."_filter_env_mod", (math.random(-10,10)/100)*size) | |
params:delta(t.."_dist", (math.random(-10,10)/100)*size) | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
--------------------------- | |
-- refresh/redraw functions | |
--------------------------- | |
function redraw() | |
screen.clear() | |
screen.aa(0) | |
screen.level(15) | |
-- grid pattern preset display | |
for i=1, 8 do | |
for n=1, tab.count(track[i]) do | |
if track[i][tab.count(track[i])][n] == 1 then | |
if mode == 1 and cursor[1] == i and cursor[3] == n and blink % 3 == 0 then | |
-- pass blinking cursor to show selection in edit mode | |
else | |
screen.rect((n-1)*7, 1 + i*7, 6, 6) | |
end | |
screen.fill() | |
screen.move(tab.count(track[i])*7, i*7 + 7) | |
screen.text(tab.count(track[i])) | |
else | |
if mode == 1 and cursor[1] == i and cursor[3] == n and blink % 3 == 0 then | |
-- pass | |
else | |
screen.rect(1 + (n-1)*7, 2 + i*7, 5, 5) | |
end | |
screen.stroke() | |
screen.move(tab.count(track[i])*7, 7 + i*7) | |
screen.text(tab.count(track[i])) | |
end | |
end | |
end | |
-- param display | |
screen.move(0,5) | |
screen.text("bpm:"..params:get("bpm")) | |
screen.move(64,5) | |
screen.level(15) | |
screen.text_center("pattern:"..pattern_select) | |
-- pause/play icon | |
if not running then | |
screen.rect(123,57,2,6) | |
screen.rect(126,57,2,6) | |
screen.fill() | |
else | |
screen.move(123,57) | |
screen.line_rel(6,3) | |
screen.line_rel(-6,3) | |
screen.fill() | |
end | |
screen.level(1) | |
-- currently selected pattern | |
screen.move(128,5) | |
screen.text_right(pattern_display) | |
screen.update() | |
end | |
function gridredraw() | |
g:all(0) | |
-- draw channels with sub divisions on/off | |
for i=1, 8 do | |
for n=1, tab.count(track[i]) do | |
ct = tab.count(track[i]) | |
if ct == 0 or nil then return | |
else | |
if i % 2 == 1 then | |
if track[i][ct][n] == 1 then | |
g:led(n, i, 12) | |
else | |
g:led(n, i, 4) | |
end | |
elseif i % 2 == 0 then | |
if track[i][ct][n] == 1 then | |
g:led(n, i, 8) | |
else | |
g:led(n, i, 2) | |
end | |
g:led((q_position % ct) + 1, i, 15) | |
end | |
end | |
end | |
end | |
g:refresh() | |
end | |
function fast_gridredraw() | |
for i=1, 8 do | |
for n=1, tab.count(track[i]) do | |
ct = tab.count(track[i]) | |
if ct == 0 or nil then return | |
else | |
if i % 2 == 0 then | |
if track[i][ct][n] == 1 then | |
g:led(n, i, 8) | |
else | |
g:led(n, i, 2) | |
end | |
g:led((q_position % ct) + 1, i, 15) | |
end | |
end | |
end | |
end | |
g:refresh() | |
end | |
---------------------- | |
-- save/load functions | |
---------------------- | |
function save_pattern() | |
tab.save(track, "/home/we/dust/data/ekombi/pattern-" .. pattern_select .. ".data") | |
print("SAVE COMPLETE") | |
end | |
function load_pattern() | |
track = tab.load("/home/we/dust/data/ekombi/pattern-".. pattern_select .. ".data") | |
print("LOAD COMPLETE") | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
update for norns 2.0