Skip to content
Permalink
master
Go to file
 
 
Cannot retrieve contributors at this time
891 lines (749 sloc) 23.1 KB
-- mangl
--
-- arc suggested.
-- grid capable.
--
-- ----------
--
-- based on the script angl
-- by @tehn and the
-- engine/script glut
-- by @artfwo
--
-- ----------
--
-- load samples via param menu
--
-- ----------
--
-- mangl is a 7 track granular
-- sample player.
--
-- arc ring 1 = speed
-- arc ring 2 = pitch
-- arc ring 3 = grain size
-- arc ring 4 = density
--
-- norns key1 = alt
-- norns key2 = enable/disable
-- voice
-- norns key3 = set speed to 0
--
-- norns enc1 = volume
-- norns enc3 = nav
--
-- ----------
--
-- holding alt and turning a ring,
-- or pressing a button,
-- performs a secondary
-- function.
--
-- alt + ring1 = scrub
-- alt + ring2 = fine tune
-- alt + ring3 = spread
-- alt + ring4 = jitter
--
-- alt + key2 = loop in/out
-- alt + key3 = loop clear
--
-- alt + enc1 = filter cutoff
-- alt + enc2 = delay send
--
-- nb: loop in/out is set in
-- one button press. loop in
-- on press, loop out on release.
--
-- ----------
--
-- @justmat v2.2
--
-- llllllll.co/t/21066
engine.name = 'MGlut'
local a = arc.connect(1)
local g = grid.connect(1)
local lfo = include 'lib/hnds_mangl'
local gridbuf = require 'lib/gridbuf'
local grid_ctl = gridbuf.new(16, 8)
local grid_voc = gridbuf.new(16, 8)
local tau = math.pi * 2
local VOICES = 7
local positions = {}
local gates = {}
local voice_levels = {}
local track_speed = {}
local loop_in = {}
local loop_out = {}
local loops = {}
local latched = {}
for i = 1, VOICES do
positions[i] = -1
track_speed[i] = 0
loop_in[i] = nil
loop_out[i] = nil
gates[i] = 0
voice_levels[i] = 0
loops[i] = {
state = 0,
dir = 1
}
latched[i] = true
end
local tracks = {"one", "two", "three", "four", "five", "six", "seven"}
local track = 1
local alt = false
local last_enc = 0
local time_last_enc = 0
local time_last_scrub = 0
local was_playing = false
local metro_grid_refresh
local metro_blink
-- arc sensitivity settings
local scrub_sens
local speed_sens
local size_sens
local density_sens
local spread_sens
local jitter_sens
-- for lib/hnds
local lfo_targets = {"none"}
for i = 1, VOICES do
table.insert(lfo_targets, i .. "position")
table.insert(lfo_targets, i .. "volume")
table.insert(lfo_targets, i .. "size")
table.insert(lfo_targets, i .. "density")
table.insert(lfo_targets, i .. "spread")
table.insert(lfo_targets, i .. "jitter")
table.insert(lfo_targets, i .. "cutoff")
table.insert(lfo_targets, i .. "send")
end
-- pattern recorder. should likely be swapped out for pattern_time lib
local pattern_banks = {}
local pattern_timers = {}
local pattern_leds = {} -- for displaying button presses
local pattern_positions = {} -- playback positions
local record_bank = -1
local record_prevtime = -1
local record_length = -1
local alt = false
local blink = 0
local metro_blink
local function record_event(x, y, z)
if record_bank > 0 then
-- record first event tick
local current_time = util.time()
if record_prevtime < 0 then
record_prevtime = current_time
end
local time_delta = current_time - record_prevtime
table.insert(pattern_banks[record_bank], {time_delta, x, y, z})
record_prevtime = current_time
end
end
local function start_playback(n)
pattern_timers[n]:start(0.001, 1) -- TODO: timer doesn't start immediately with zero
end
local function stop_playback(n)
pattern_timers[n]:stop()
pattern_positions[n] = 1
end
local function arm_recording(n)
record_bank = n
end
local function stop_recording()
local recorded_events = #pattern_banks[record_bank]
if recorded_events > 0 then
-- save last delta to first event
local current_time = util.time()
local final_delta = current_time - record_prevtime
pattern_banks[record_bank][1][1] = final_delta
start_playback(record_bank)
end
record_bank = -1
record_prevtime = -1
end
local function pattern_next(n)
local bank = pattern_banks[n]
local pos = pattern_positions[n]
local event = bank[pos]
local delta, x, y, z = table.unpack(event)
pattern_leds[n] = z
grid_key(x, y, z, true)
local next_pos = pos + 1
if next_pos > #bank then
next_pos = 1
end
local next_event = bank[next_pos]
local next_delta = next_event[1]
pattern_positions[n] = next_pos
-- schedule next event
pattern_timers[n]:start(next_delta, 1)
end
local function record_handler(n)
if alt then
-- clear pattern
if n == record_bank then stop_recording() end
if pattern_timers[n].is_running then stop_playback(n) end
pattern_banks[n] = {}
do return end
end
if n == record_bank then
-- stop if pressed current recording
stop_recording()
else
local pattern = pattern_banks[n]
if #pattern > 0 then
-- toggle playback if there's data
if pattern_timers[n].is_running then stop_playback(n) else start_playback(n) end
else
-- stop recording if it's happening
if record_bank > 0 then
stop_recording()
end
-- arm new pattern for recording
arm_recording(n)
end
end
end
-- internals
local function get_sample_name()
-- strips the path and extension from filenames
-- if filename is over 15 chars, returns a folded filename
local long_name = string.match(params:get(track .. "sample"), "[^/]*$")
local short_name = string.match(long_name, "(.+)%..+$")
if string.len(short_name) >= 15 then
return string.sub(short_name, 1, 4) .. '...' .. string.sub(short_name, -4)
else
return short_name
end
end
local function display_voice(phase, width)
local pos = phase * width
local levels = {}
for i = 1, width do levels[i] = 0 end
local left = math.floor(pos)
local index_left = left + 1
local dist_left = math.abs(pos - left)
local right = math.floor(pos + 1)
local index_right = right + 1
local dist_right = math.abs(pos - right)
if index_left < 1 then index_left = width end
if index_left > width then index_left = 1 end
if index_right < 1 then index_right = width end
if index_right > width then index_right = 1 end
levels[index_left] = math.floor(math.abs(1 - dist_left) * 15)
levels[index_right] = math.floor(math.abs(1 - dist_right) * 15)
return levels
end
local function start_voice(voice, pos)
engine.seek(voice, pos)
engine.gate(voice, 1)
gates[voice] = 1
end
local function stop_voice(voice)
gates[voice] = 0
engine.gate(voice, 0)
end
local function set_speed(n)
params:set(n .. "speed", track_speed[n])
end
local function hold_track_speed(n)
-- remember track speed and direction while scrubbing audio file
local speed = params:get(n .. "speed")
if speed ~= 0 then
track_speed[n] = speed
if speed < 0 then
loops[n].dir = -1
else
loops[n].dir = 1
end
end
end
local function scrub(n, d)
-- scrub playback position
hold_track_speed(n)
params:set(n .. "speed", 0)
was_playing = true
engine.seek(n, positions[n] + d / scrub_sens)
end
local function clear_loop(track)
loop_in[track] = nil
loop_out[track] = nil
loops[track].state = 0
end
function loop_pos()
-- keeps playback inside the loop
for i = 1, VOICES do
if loops[i].state == 1 then
if loops[i].dir == -1 then
if positions[i] <= loop_in[i] then
positions[i] = loop_out[i]
engine.seek(i, loop_out[i])
end
else
if positions[i] >= loop_out[i] then
positions[i] = loop_in[i]
engine.seek(i, loop_in[i])
end
end
end
end
end
-- for hnds
function lfo.process()
-- for lib hnds
for i = 1, 4 do
local target = params:get(i .. "lfo_target")
local target_name = string.sub(lfo_targets[target], 2)
local voice = string.sub(lfo_targets[target], 1, 1)
if params:get(i .. "lfo") == 2 then
-- volume
if target_name == "volume" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, -60, 20))
-- size
elseif target_name == "size" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, 1, 500))
-- density
elseif target_name == "density" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, 0, 512))
-- spread
elseif target_name == "spread" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, 0, 100))
-- jitter
elseif target_name == "jitter" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, 0, 500))
-- position
elseif target_name == "position" then
engine.seek(voice, lfo.scale(lfo[i].slope, -1.0, 2.0, 0, 1))
-- filter cutoff
elseif target_name == "cutoff" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, 0, 20000))
-- delay send
elseif target_name == "send" then
params:set(lfo_targets[target], lfo.scale(lfo[i].slope, -1.0, 2.0, 0.00, 1.00))
end
end
end
end
-- init ----------
function init()
g.key = function(x, y, z)
grid_key(x, y, z)
end
-- polls
for v = 1, VOICES do
local phase_poll = poll.set('phase_' .. v, function(pos) positions[v] = pos end)
phase_poll.time = 0.025
phase_poll:start()
local level_poll = poll.set('level_' .. v, function(lvl) voice_levels[v] = lvl end)
level_poll.time = 0.05
level_poll:start()
end
-- recorders
for v = 1, VOICES do
table.insert(pattern_timers, metro.init(function(tick) pattern_next(v) end))
table.insert(pattern_banks, {})
table.insert(pattern_leds, 0)
table.insert(pattern_positions, 1)
end
params:add_separator("load samples")
local sep = ": "
--params:add_group("samples", VOICES)
for i = 1, VOICES do
params:add_file(i .. "sample", i .. sep .. "sample")
params:set_action(i .. "sample", function(file) engine.read(i, file) end)
end
params:add_separator("mangl params")
for v = 1, VOICES do
params:add_group("voice " .. v, 14)
params:add_option(v .. "play", "play", {"off","on"}, 1)
params:set_action(v .. "play", function(x) engine.gate(v, x-1) end)
params:add_taper(v .. "gain", "gain", -60, 20, -12, 0, "dB")
params:set_action(v .. "gain", function(value) engine.gain(v, math.pow(10, value / 20)) end)
params:add_control(v .. "pos", "pos", controlspec.new(0, 1, "lin", 0.001, 0))
params:set_action(v .. "pos", function(value) engine.seek(v, value) end)
params:add_taper(v .. "speed", "speed", -300, 300, 0, 0, "%")
params:set_action(v .. "speed", function(value) engine.speed(v, value / 100) end)
params:add_taper(v .. "jitter", "jitter", 0, 500, 0, 5, "ms")
params:set_action(v .. "jitter", function(value) engine.jitter(v, value / 1000) end)
params:add_taper(v .. "size", "size", 1, 500, 100, 5, "ms")
params:set_action(v .. "size", function(value) engine.size(v, value / 1000) end)
params:add_taper(v .. "density", "density", 0, 512, 20, 6, "hz")
params:set_action(v .. "density", function(value) engine.density(v, value) end)
params:add_control(v .. "density_mod_amt", "density mod amt", controlspec.new(0, 1, "lin", 0, 0))
params:set_action(v .. "density_mod_amt", function(value) engine.density_mod_amt(v, value) end)
params:add_taper(v .. "pitch", "pitch", -24, 24, 0, 0, "st")
params:set_action(v .. "pitch", function(value) engine.pitch(v, math.pow(0.5, -value / 12)) end)
params:add_taper(v .. "spread", "spread", 0, 100, 0, 0, "%")
params:set_action(v .. "spread", function(value) engine.spread(v, value / 100) end)
params:add_control(v .. "cutoff", "filter cutoff", controlspec.new(20, 20000, "exp", 0, 20000, "hz"))
params:set_action(v .. "cutoff", function(value) engine.cutoff(v, value) end)
params:add_control(v .. "q", "filter q", controlspec.new(0.00, 1.00, "lin", 0.01, 1))
params:set_action(v .. "q", function(value) engine.q(v, value) end)
params:add_control(v .. "send", "delay send", controlspec.new(0.0, 1.0, "lin", 0.01, .2))
params:set_action(v .. "send", function(value) engine.send(v, value) end)
params:add_taper(v .. "fade", "att / dec", 1, 9000, 1000, 3, "ms")
params:set_action(v .. "fade", function(value) engine.envscale(v, value / 1000) end)
end
params:add_group("delay", 8)
-- effect controls
-- delay time
params:add_control("delay_time", "*" .. "delay time", controlspec.new(0.0, 60.0, "lin", .01, 2.00, ""))
params:set_action("delay_time", function(value) engine.delay_time(value) end)
-- delay size
params:add_control("delay_size", "*" .. "delay size", controlspec.new(0.5, 5.0, "lin", 0.01, 2.00, ""))
params:set_action("delay_size", function(value) engine.delay_size(value) end)
-- dampening
params:add_control("delay_damp", "*" .. "delay damp", controlspec.new(0.0, 1.0, "lin", 0.01, 0.10, ""))
params:set_action("delay_damp", function(value) engine.delay_damp(value) end)
-- diffusion
params:add_control("delay_diff", "*" .. "delay diff", controlspec.new(0.0, 1.0, "lin", 0.01, 0.707, ""))
params:set_action("delay_diff", function(value) engine.delay_diff(value) end)
-- feedback
params:add_control("delay_fdbk", "*" .. "delay fdbk", controlspec.new(0.00, 1.0, "lin", 0.01, 0.20, ""))
params:set_action("delay_fdbk", function(value) engine.delay_fdbk(value) end)
-- mod depth
params:add_control("delay_mod_depth", "*" .. "delay mod depth", controlspec.new(0.0, 1.0, "lin", 0.01, 0.00, ""))
params:set_action("delay_mod_depth", function(value) engine.delay_mod_depth(value) end)
-- mod rate
params:add_control("delay_mod_freq", "*" .. "delay mod freq", controlspec.new(0.0, 10.0, "lin", 0.01, 0.10, "hz"))
params:set_action("delay_mod_freq", function(value) engine.delay_mod_freq(value) end)
-- delay output volume
params:add_control("delay_volume", "*" .. "delay output volume", controlspec.new(0.0, 1.0, "lin", 0, 1.0, ""))
params:set_action("delay_volume", function(value) engine.delay_volume(value) end)
-- for hnds
for i = 1, 4 do
lfo[i].lfo_targets = lfo_targets
end
params:add_group("lfo's", 28)
lfo.init()
-- arc sensitivty settings
params:add_separator("hardware config")
params:add_option("alt_behavior", "alt behavior", {"momentary", "toggle"}, 1)
params:add_number("speed_sens", "speed sens" .. sep, 1, 100, 10)
params:set_action("speed_sens", function(x) speed_sens = x end)
params:add_number("size_sens", "size sens" .. sep, 1, 100, 10)
params:set_action("size_sens", function(x) size_sens = x end)
params:add_number("density_sens", "density sens" .. sep, 1, 100, 10)
params:set_action("density_sens", function(x) density_sens = x end)
params:add_number("scrub_sens", "scrub sens" .. sep, 100, 1000, 500)
params:set_action("scrub_sens", function(x) scrub_sens = x end)
params:add_number("spread_sens", "spread sens" .. sep, 1, 100, 10)
params:set_action("spread_sens", function(x) spread_sens = x end)
params:add_number("jitter_sens", "jitter sens" .. sep, 1, 100, 10)
params:set_action("jitter_sens", function(x) jitter_sens = x end)
params:read()
params:bang()
-- grid refresh timer, 40 fps
metro_grid_refresh = metro.init(function(stage) grid_refresh() end, 1 / 40)
metro_grid_refresh:start()
metro_blink = metro.init(function(stage) blink = blink ~ 1 end, 1 / 4)
metro_blink:start()
-- arc redraw metro
local arc_redraw_timer = metro.init()
arc_redraw_timer.time = 0.025
arc_redraw_timer.event = function() arc_redraw() end
arc_redraw_timer:start()
-- norns redraw metro
local norns_redraw_timer = metro.init()
norns_redraw_timer.time = 0.025
norns_redraw_timer.event = function() redraw() end
norns_redraw_timer:start()
-- loop metro
local loop_timer = metro.init()
loop_timer.time = 0.005
loop_timer.event = function() loop_pos() end
loop_timer:start()
norns.enc.sens(3, 8)
norns.enc.accel(3, false)
end
-- norns
function key(n, z)
if n == 1 then
hold_track_speed(track)
if params:get("alt_behavior") == 1 then
alt = z == 1 and true or false
elseif z == 1 and params:get("alt_behavior") == 2 then
alt = not alt
end
end
if alt then
-- key 2 sets the loop_in and loop_out points
-- loop_in on press, loop_out on release
if n == 2 then
if z == 1 then
if loop_in[track] == nil then
if loops[track].dir == -1 then
loop_out[track] = positions[track]
else
loop_in[track] = positions[track]
end
end
else
if loops[track].dir == -1 then
loop_in[track] = positions[track]
positions[track] = loop_out[track]
engine.seek(track, loop_out[track])
loops[track].state = 1
else
loop_out[track] = positions[track]
positions[track] = loop_in[track]
engine.seek(track, loop_in[track])
loops[track].state = 1
end
end
-- key 3 clears the currently selected track
elseif n == 3 then
clear_loop(track)
end
else
if was_playing then
set_speed(track)
was_playing = false
end
-- key 2 activates and deactivates the currently selected voice
if n == 2 and z == 1 then
params:set(track .. "play", params:get(track .. "play") == 2 and 1 or 2)
-- key 3 sets speed to 0
elseif n == 3 and z == 1 then
params:set(track .. "speed", 0)
end
end
end
function enc(n, d)
if alt then
if n == 1 then
params:delta(track .. "cutoff", d)
elseif n == 2 then
params:delta(track .. "send", d)
end
else
if n == 1 then
params:delta(track .. "gain", d)
elseif n == 3 then
track = util.clamp(track + d, 1, 7)
end
end
last_enc = n
time_last_enc = util.time()
end
function redraw()
screen.aa(1)
screen.clear()
screen.move(123, 10)
screen.font_face(25)
screen.font_size(6)
screen.level(2)
if alt then
screen.text_right(params:get(track .. "send"))
else
if params:get(track .. "sample") == "-" then
screen.level(1)
screen.text_right("-")
else
screen.text_right(get_sample_name())
end
end
screen.move(64, 36)
screen.level(params:get(track .. "play") == 2 and 15 or 3)
screen.font_face(10)
screen.font_size(30)
screen.text_center(tracks[track])
screen.level(2)
screen.move(20, 10)
screen.font_face(25)
screen.font_size(6)
if alt then
screen.text_center(string.format("%.2f", params:get(track .. "cutoff")))
else
screen.text_center(string.format("%.2f", params:get(track .. "gain")))
end
screen.move(20, 50)
screen.font_size(6)
screen.font_face(25)
screen.level(2)
screen.text_center(alt and "scrub" or "speed")
screen.move(50, 50)
screen.text_center(alt and "fine" or "pitch")
screen.move(80, 50)
screen.text_center(alt and "spread" or "size")
screen.move(110, 50)
screen.text_center(alt and "jitter" or "density")
screen.level(params:get(track .. "play") == 2 and 15 or 3)
screen.move(20, 60)
screen.font_size(8)
screen.font_face(1)
screen.text_center(alt and "-" or string.format("%.2f", params:get(track .. "speed")))
screen.move(50, 60)
screen.text_center(string.format("%.2f", params:get(track .. "pitch")))
screen.move(80, 60)
screen.text_center(string.format("%.2f", alt and params:get(track .. "spread") or params:get(track .. "size")))
screen.move(110, 60)
screen.text_center(string.format("%.2f", alt and params:get(track .. "jitter") or params:get(track .. "density")))
if track == 3 then
screen.move(100, 36)
elseif track == 6 then
screen.move(84, 36)
elseif track == 7 then
screen.move(103, 36)
else
screen.move(90, 36)
end
screen.level(loops[track].state == 1 and 12 or 0)
screen.font_size(12)
screen.font_face(12)
screen.text("L")
if not latched[track] then
screen.level(12)
screen.font_face(12)
screen.font_size(8)
screen.move_rel(-6, -11)
screen.text("m")
end
screen.update()
end
-- arc ----------
function a.delta(n, d)
if alt then
if n == 1 then
scrub(track, d)
elseif n == 2 then
params:delta(track .. "pitch", d / 20)
elseif n == 3 then
params:delta(track .. "spread", d / spread_sens)
elseif n == 4 then
params:delta(track .. "jitter", d / jitter_sens)
end
else
if n == 1 then
params:delta(track .. "speed", d / speed_sens)
elseif n == 2 then
params:delta(track .. "pitch", d / 2)
elseif n == 3 then
params:delta(track .. "size", d / size_sens)
elseif n == 4 then
params:delta(track .. "density", d / density_sens)
end
end
end
function a.key(n, z)
-- for old push button arcs and 4e support
alt = z == 1 and true or false
if not alt and was_playing then
set_speed(track)
end
end
function arc_redraw()
a:all(0)
a:segment(1, positions[track] * tau, tau * positions[track] + 0.2, 15)
local pitch = params:get(track .. "pitch") / 10
if pitch > 0 then
a:segment(2, 0.5, 0.5 + pitch, 15)
else
a:segment(2, pitch - 0.5, -0.5, 15)
end
if alt == true then
local spread = params:get(track .. "spread") / 40
local jitter = params:get(track .. "jitter") / 80
a:segment(3, 0.5, 0.5 + spread, 15)
a:segment(4, 0.5, 0.5 + jitter, 15)
else
local size = params:get(track .. "size") / 80
local density = params:get(track .. "density") / 82
a:segment(3, 0.5, 0.5 + size, 15)
a:segment(4, 0.5, 0.5 + density, 15)
end
a:refresh()
end
-- grid ----------
function grid_key(x, y, z, skip_record)
if y > 1 or (y == 1 and x < 9) then
if not skip_record then
record_event(x, y, z)
end
end
-- track selection via grid press
if y >= 2 and not skip_record then
track = y - 1
if alt and z == 1 then
params:set(track .. "play", 2)
latched[track] = not latched[track]
else
if latched[track] == false then
params:set(track .. "play", z == 1 and 2 or 1)
end
end
end
if z > 0 then
-- set voice pos
if y > 1 then
local voice = y - 1
start_voice(voice, (x - 1) / 16)
params:set(voice .. "play", 2)
else
if x == 16 then
-- alt
alt = true
elseif x > 8 then
record_handler(x - 8)
elseif x == 8 then
-- reserved
elseif x < 8 then
local voice = x
if alt then
-- stop
stop_voice(voice)
params:set(voice .. "play", 1)
else
track = voice
end
end
end
else
-- alt
if x == 16 and y == 1 then alt = false end
end
end
function grid_refresh()
if g == nil then
return
end
grid_ctl:led_level_all(0)
grid_voc:led_level_all(0)
-- alt
grid_ctl:led_level_set(16, 1, alt and 15 or 2)
-- pattern banks
for i=1, VOICES do
local level = 4
if #pattern_banks[i] > 0 then level = 8 end
if pattern_timers[i].is_running then
level = 12
if pattern_leds[i] > 0 then
level = 12
end
end
grid_ctl:led_level_set(8 + i, 1, level)
end
-- blink armed pattern
if record_bank > 0 then
grid_ctl:led_level_set(8 + record_bank, 1, 15 * blink)
end
-- voices
for i=1, VOICES do
if i == track then
grid_ctl:led_level_set(i, 1, 4)
else
grid_ctl:led_level_set(i, 1, 2)
end
if voice_levels[i] > 0 then
grid_ctl:led_level_set(i, 1, math.min(math.ceil(voice_levels[i] * 15), 15))
grid_voc:led_level_row(1, i + 1, display_voice(positions[i], 16))
end
end
local buf = grid_ctl | grid_voc
buf:render(g)
g:refresh()
end
function cleanup()
poll.clear_all()
end