Skip to content
Permalink
master
Go to file
 
 
Cannot retrieve contributors at this time
1025 lines (917 sloc) 25.3 KB
local Beets = {}
Beets.__index = Beets
local ControlSpec = require 'controlspec'
local Formatters = require 'formatters'
local RENDER_SAMPLES = 72
local BREAK_OFFSET = 5
local VOICE_OFFSET = 100
local EVENT_ORDER = {'<', '>', 'R', 'S', 'B'}
local PROBABILITY_ORDER = {
'jump_back',
'jump',
'reverse',
'stutter',
'loop_index_jump'
}
local json = include('lib/json')
local inspect = include('lib/inspect')
local layout = {
horiz_spacing = 9,
vert_spacing = 9,
left_margin = 10,
top_margin = 23,
sidebar_top_margin = 10
}
function Beets.new(options)
local softcut_voice_id = options.softcut_voice_id or 1
local i = {
-- descriptive global state
debug = false,
running = false,
enable_mutations = true,
id = softcut_voice_id,
beat_count = 8,
loops_by_filename = {},
loop_index_to_filename = {},
loop_count = 0,
editing = false,
editing_mode = {cursor_location = 0},
amplitude = 1,
-- state that changes on the beat
beatstep = 0,
index = 0,
played_index = 0,
played_loop_index = 0,
message = '',
status = '',
events = {},
muted = false,
current_bpm = 0,
beat_start = 0,
beat_end = 7,
loop_index = 1,
on_beat_one = function()
end,
on_beat = function()
end,
on_kick = function()
end,
on_snare = function()
end,
-- probability values
probability = {
loop_index_jump = 0,
stutter = 0,
reverse = 0,
jump = 0,
jump_back = 0
},
ui = {slice_buttons_down = {}, mute_button = 0, shift_button = 0}
}
setmetatable(i, Beets)
return i
end
function Beets:advance_step(in_beatstep, in_bpm)
self.events = {}
self.message = ''
self.status = ''
self.beatstep = in_beatstep
self.current_bpm = in_bpm
if not self.running then
self.status = 'NOT RUNNING'
return
end
if self.loop_count == 0 then
self.status = 'LOAD LOOPS IN PARAMS'
return
end
if self.muted then
self.status = 'MUTED'
softcut.level(self.id, 0)
else
softcut.level(self.id, self.amplitude)
end
if self.editing then
-- play the current edit position slice every other beat
-- so that it's easier to hear what the sound is at the start of the slice
if self.beatstep % 4 ~= 0 then
self:play_nothing()
else
local edit_index = math.floor(self.editing_mode.cursor_location)
self:play_slice(edit_index)
end
return
end
if self.beatstep == 0 then
self.on_beat_one()
end
self:calculate_next_slice()
self:play_slice(self.index)
self.played_index = self.index
end
function Beets:instant_toggle_mute()
self:toggle_mute()
if self.muted then
softcut.level(self.id, 0)
else
softcut.level(self.id, self.amplitude)
end
end
function Beets:mute(in_muted)
if in_muted then
self.muted = true
else
self.muted = false
end
end
function Beets:toggle_mute()
self:mute(not self.muted)
end
function Beets:should(thing)
if not self.enable_mutations then
return false
end
return math.random(100) <= self.probability[thing]
end
function Beets:play_nothing()
softcut.level(self.id, 0)
end
function Beets:random_loop_index()
local timeout = self.loop_count
local l = math.random(self.loop_count)
while timeout > 0 do
local loop = self:loop_at_index(l)
if loop.enabled == 1 then
return l
end
l = l + 1
if l > self.loop_count then
l = 1
end
end
return 1
end
function Beets:play_slice(slice_index)
if (self:should('loop_index_jump')) then
params:set(self.id .. '_' .. 'loop_index', self:random_loop_index())
self.events['B'] = 1
else
self.events['B'] = 0
end
self.played_loop_index = self.loop_index
local loop = self:loop_at_index(self.played_loop_index)
local current_rate = loop.rate * (self.current_bpm / loop.bpm)
if (self:should('stutter')) then
self.events['S'] = 1
local stutter_amount = math.random(4)
softcut.loop_start(self.id, loop.start + (slice_index * (loop.duration / self.beat_count)))
softcut.loop_end(
self.id,
loop.start + (slice_index * (loop.duration / self.beat_count) + (loop.duration / (64.0 / stutter_amount)))
)
else
self.events['S'] = 0
softcut.loop_start(self.id, loop.start)
softcut.loop_end(self.id, loop.start + loop.duration)
end
if (self:should('reverse')) then
self.events['R'] = 1
softcut.rate(self.id, 0 - current_rate)
else
self.events['R'] = 0
softcut.rate(self.id, current_rate)
end
local position = loop.start + (slice_index * (loop.duration / self.beat_count))
softcut.position(self.id, position)
if not self.editing then
self:notify_beat(loop.beat_types[slice_index + 1])
end
end
function Beets:notify_beat(beat_type)
if beat_type == 'K' then
self.on_kick()
end
if beat_type == 'S' then
self.on_snare()
end
end
function Beets:toggle_loop_enabled(index)
local loop = self:loop_at_index(index)
if loop.enabled == 1 then
loop.enabled = 0
elseif loop.enabled == 0 then
loop.enabled = 1
end
end
function Beets:toggle_slice_enabled(slice_index)
local loop = self:loop_at_index(self.loop_index)
if loop.beat_enabled[slice_index + 1] == 1 then
loop.beat_enabled[slice_index + 1] = 0
elseif loop.beat_enabled[slice_index + 1] == 0 then
loop.beat_enabled[slice_index + 1] = 1
end
end
function Beets:slice_is_enabled(slice_index)
local loop = self:loop_at_index(self.loop_index)
return loop.beat_enabled[slice_index + 1] == 1
end
function Beets:next_loop(loop_index, direction)
local new_index = loop_index
local timeout = self.loop_count
while timeout > 0 do
new_index = new_index + direction
if new_index == 0 then
new_index = self.loop_count
end
if new_index > self.loop_count then
new_index = 1
end
local loop = self:loop_at_index(new_index)
if loop.enabled == 1 then
return new_index
end
timeout = timeout - 1
end
end
function Beets:step_forward(index)
local timeout = self.beat_count
local new_index = index
while timeout > 0 do
new_index = new_index + 1
if new_index > self.beat_end then
new_index = self.beat_start
if params:get(self.id .. '_' .. 'auto_advance') == 2 then
self.loop_index = self:next_loop(self.loop_index, 1)
end
end
if self:slice_is_enabled(new_index) then
return new_index
end
timeout = timeout - 1
end
return 0
end
function Beets:step_backward(index)
local timeout = self.beat_count
local new_index = index
while timeout > 0 do
new_index = new_index - 1
if new_index < self.beat_start then
new_index = self.beat_end
if params:get(self.id .. '_' .. 'auto_advance') == 2 then
self.loop_index = self:next_loop(self.loop_index, -1)
end
end
if self:slice_is_enabled(new_index) then
return new_index
end
timeout = timeout - 1
end
return 0
end
function Beets:step_first()
local new_index = self.beat_start
if self:slice_is_enabled(new_index) then
return new_index
end
return self:step_forward(new_index)
end
function Beets:calculate_next_slice()
local new_index = self:step_forward(self.index)
if (self:should('jump')) then
self.events['>'] = 1
new_index = self:step_forward(new_index)
else
self.events['>'] = 0
end
if (self:should('jump_back')) then
self.events['<'] = 1
new_index = self:step_backward(new_index)
else
self.events['<'] = 0
end
if (self.beatstep == 0) then
new_index = self:step_first()
end
self.index = new_index
end
function Beets:clear_loops()
self.loop_index_to_filename = {}
self.loops_by_filename = {}
self.loop_count = 0
end
function Beets:load_directory(path)
self:clear_loops()
local f = io.popen('ls "' .. path .. '"/*.wav')
local filenames = {}
for name in f:lines() do
table.insert(filenames, name)
end
table.sort(filenames)
for i, name in ipairs(filenames) do
self:load_loop(i, {file = name})
i = i + 1
end
end
function Beets:save_loop_info(loop_info)
local json_filename = loop_info.filename .. '.json'
local f = io.open(json_filename, 'w')
f:write(json.encode(loop_info))
f:close()
end
function Beets:load_loop(index, loop)
local filename = loop.file
local kicks = loop.kicks
local snares = loop.snares
local loop_info = {}
local json_filename = filename .. '.json'
local f = io.open(json_filename)
if f ~= nil then
loop_info = json.decode(f:read('*a'))
else
local ch, samples, samplerate = audio.file_info(filename)
loop_info.frames = samples
loop_info.rate = samplerate / 48000.0 -- compensate for files that aren't 48Khz
loop_info.duration = samples / 48000.0
loop_info.beat_types = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}
loop_info.filename = filename
if kicks then
for _, beat in ipairs(kicks) do
loop_info.beat_types[beat + 1] = 'K'
end
end
if snares then
for _, beat in ipairs(snares) do
loop_info.beat_types[beat + 1] = 'S'
end
end
self:save_loop_info(loop_info)
end
loop_info.bpm = (4 * 60) / loop_info.duration
loop_info.start = index * BREAK_OFFSET + self.id * VOICE_OFFSET
loop_info.index = index
loop_info.enabled = 1
loop_info.beat_enabled = {1, 1, 1, 1, 1, 1, 1, 1}
loop_info.waveform_samples = {}
softcut.buffer_read_mono(filename, 0, loop_info.start, -1, 1, 1)
softcut.render_buffer(self.id, loop_info.start, loop_info.duration, RENDER_SAMPLES)
self.loop_index_to_filename[index] = filename
self.loops_by_filename[filename] = loop_info
self.loop_count = index
self:reset_loop_index_param()
end
function Beets:softcut_init()
softcut.enable(self.id, 1)
softcut.buffer(self.id, 1)
softcut.level(self.id, self.amplitude)
softcut.level_slew_time(self.id, 0.2)
softcut.loop(self.id, 1)
softcut.loop_start(self.id, 0)
softcut.loop_end(self.id, 0)
softcut.position(self.id, 0)
softcut.rate(self.id, 0)
softcut.play(self.id, 1)
softcut.fade_time(self.id, 0.010)
softcut.post_filter_dry(self.id, 0.0)
softcut.post_filter_lp(self.id, 1.0)
softcut.post_filter_rq(self.id, 0.3)
softcut.post_filter_fc(self.id, 44100)
end
function Beets:start()
self:softcut_init()
self.running = true
end
function Beets:stop()
self.running = false
softcut.play(self.id, 0)
end
function Beets:reset_loop_index_param()
for _, p in ipairs(params.params) do
if p.id == self.id .. '_' .. 'loop_index' then
p.controlspec = ControlSpec.new(1, self.loop_count, 'lin', 1, 1, '')
end
end
end
function Beets:add_params(arcify)
local specs = {}
specs.AMP = ControlSpec.new(0, 1, 'lin', 0, 1, '')
specs.FILTER_FREQ = ControlSpec.new(20, 20000, 'exp', 0, 20000, 'Hz')
specs.FILTER_RESONANCE = ControlSpec.new(0.05, 1, 'lin', 0, 0.25, '')
specs.PERCENTAGE = ControlSpec.new(0, 1, 'lin', 0.01, 0, '%')
specs.BEAT_START = ControlSpec.new(0, self.beat_count - 1, 'lin', 1, 0, '')
specs.BEAT_END = ControlSpec.new(0, self.beat_count - 1, 'lin', 1, self.beat_count - 1, '')
local files = {}
local files_count = 0
local loops_dir = _path.audio .. 'beets/'
local f = io.popen('cd ' .. loops_dir .. '; ls -d *')
for name in f:lines() do
table.insert(files, name)
files_count = files_count + 1
end
table.sort(files)
local name
if files_count == 0 then
name = 'Create folders in audio/beets to load'
self.loops_folder_name = '-'
else
name = 'Loops folder'
self.loops_folder_name = files[1]
end
params:add_group('Voice ' .. self.id, 16)
params:add {
type = 'option',
id = self.id .. '_' .. 'dir_chooser',
name = name,
options = files,
action = function(value)
self.loops_folder_name = files[value]
end
}
params:add {
type = 'trigger',
id = self.id .. '_' .. 'load_loops',
name = 'Load loops',
action = function(value)
if value == '-' then
return
end
self:load_directory(_path.audio .. 'beets/' .. self.loops_folder_name)
end
}
params:add_separator()
params:add {
type = 'control',
id = self.id .. '_' .. 'amplitude',
name = 'Amplitude',
controlspec = specs.AMP,
default = 1.0,
action = function(value)
self.amplitude = value
softcut.level(self.id, self.amplitude)
end
}
arcify:register(self.id .. '_' .. 'amplitude')
params:add {
type = 'control',
id = self.id .. '_' .. 'pan',
name = 'Pan',
controlspec = ControlSpec.PAN,
formatter = Formatters.bipolar_as_pan_widget,
default = 0.5,
action = function(value)
self.pan = value
softcut.pan(self.id, self.pan)
end
}
arcify:register(self.id .. '_' .. 'pan')
params:add {
type = 'option',
id = self.id .. '_' .. 'auto_advance',
name = 'Auto-advance loop',
options = {'off', 'on'}
}
params:add {
type = 'control',
id = self.id .. '_' .. 'loop_index',
name = 'Sample',
controlspec = ControlSpec.new(1, self.loop_count, 'lin', 1, 1, ''),
action = function(value)
self.loop_index = value
self:loop_at_index(self.loop_index).enabled = 1
end
}
arcify:register(self.id .. '_' .. 'loop_index', 0.05)
params:add {
type = 'control',
id = self.id .. '_' .. 'jump_back_probability',
name = 'Jump Back Probability',
controlspec = specs.PERCENTAGE,
formatter = Formatters.percentage,
action = function(value)
self.probability.jump_back = value * 100
end
}
arcify:register(self.id .. '_' .. 'jump_back_probability')
params:add {
type = 'control',
id = self.id .. '_' .. 'jump_probability',
name = 'Jump Probability',
controlspec = specs.PERCENTAGE,
formatter = Formatters.percentage,
action = function(value)
self.probability.jump = value * 100
end
}
arcify:register(self.id .. '_' .. 'jump_probability')
params:add {
type = 'control',
id = self.id .. '_' .. 'reverse_probability',
name = 'Reverse Probability',
controlspec = specs.PERCENTAGE,
formatter = Formatters.percentage,
action = function(value)
self.probability.reverse = value * 100
end
}
arcify:register(self.id .. '_' .. 'reverse_probability')
params:add {
type = 'control',
id = self.id .. '_' .. 'stutter_probability',
name = 'Stutter Probability',
controlspec = specs.PERCENTAGE,
formatter = Formatters.percentage,
action = function(value)
self.probability.stutter = value * 100
end
}
arcify:register(self.id .. '_' .. 'stutter_probability')
params:add {
type = 'control',
id = self.id .. '_' .. 'loop_index_jump_probability',
name = 'Loop Jump Probability',
controlspec = specs.PERCENTAGE,
formatter = Formatters.percentage,
action = function(value)
self.probability.loop_index_jump = value * 100
end
}
arcify:register(self.id .. '_' .. 'loop_index_jump_probability')
params:add {
type = 'control',
id = self.id .. '_' .. 'filter_frequency',
name = 'Filter Cutoff',
controlspec = specs.FILTER_FREQ,
formatter = Formatters.format_freq,
action = function(value)
softcut.post_filter_fc(self.id, value)
end
}
arcify:register(self.id .. '_' .. 'filter_frequency', 10.0)
params:add {
type = 'control',
id = self.id .. '_' .. 'filter_reso',
name = 'Filter Resonance',
controlspec = specs.FILTER_RESONANCE,
action = function(value)
softcut.post_filter_rq(self.id, value)
end
}
arcify:register(self.id .. '_' .. 'filter_reso', 0.1)
params:add {
type = 'control',
id = self.id .. '_' .. 'beat_start',
name = 'Beat Start',
controlspec = specs.BEAT_START,
action = function(value)
self.beat_start = value
end
}
arcify:register(self.id .. '_' .. 'beat_start', 0.05)
params:add {
type = 'control',
id = self.id .. '_' .. 'beat_end',
name = 'Beat End',
controlspec = specs.BEAT_END,
action = function(value)
self.beat_end = value
end
}
arcify:register(self.id .. '_' .. 'beat_end', 0.05)
end
function Beets:_drawCurrentLoopGrid(options)
local played_index = options.played_index or self.played_index
local beatstep = options.beatstep or self.beatstep
local loop_index = options.loop_index or self.loop_index
local loop = self.loops_by_filename[self.loop_index_to_filename[loop_index]]
local scale = 10
local x_pos = 1
for i,s in ipairs(loop.waveform_samples) do
if played_index == math.floor(i / (RENDER_SAMPLES / 8)) then
screen.level(15)
else
screen.level(2)
end
local height = util.round(math.abs(s) * (scale))
screen.move(util.linlin(0,RENDER_SAMPLES,layout.left_margin,layout.left_margin + layout.horiz_spacing * 8 - 1,x_pos), 10 - height)
screen.line_rel(0, 2 * height)
screen.stroke()
x_pos = x_pos + 1
end
for i = 0, 7 do
screen.rect(
layout.left_margin + layout.horiz_spacing * i,
layout.top_margin,
layout.horiz_spacing,
layout.vert_spacing
)
if played_index == i then
screen.level(15)
elseif beatstep == i then
screen.level(2)
else
screen.level(0)
end
screen.fill()
screen.rect(
layout.left_margin + layout.horiz_spacing * i,
layout.top_margin,
layout.horiz_spacing,
layout.vert_spacing
)
screen.level(1)
screen.move(layout.left_margin + layout.horiz_spacing * i + 2, layout.top_margin + 6)
screen.text(loop.beat_types[i + 1])
screen.level(2)
screen.stroke()
screen.level(15)
end
end
function Beets:grid_key(x, y, z)
if self.loop_count == 0 or self.editing then
return
end
if x == 8 and y == 8 then
self.ui.mute_button = z
if z == 0 then
self:toggle_mute()
end
redraw()
end
if x == 1 and y == 3 then
self.ui.shift_button = z
redraw()
end
if z == 1 and x == 8 and y == 3 then -- auto_advance
local current_auto_advance = params:get(self.id .. '_' .. 'auto_advance')
if current_auto_advance == 1 then
params:set(self.id .. '_' .. 'auto_advance', 2)
else
params:set(self.id .. '_' .. 'auto_advance', 1)
end
end
if y == 1 and x <= self.beat_count then
if self.ui.shift_button == 1 then
if z == 1 then
self:toggle_slice_enabled(x - 1)
end
elseif z == 1 then
self.ui.slice_buttons_down[x] = 1
local count = 0
local first, second
for button_down in pairs(self.ui.slice_buttons_down) do
if first == nil then
first = button_down
else
if button_down > first then
second = button_down
else
second = first
first = button_down
end
end
count = count + 1
end
if count == 1 then -- for double-tap single-button-loop handling
if self.ui.slice_button_saved then
if self.ui.slice_button_saved == x then
-- DOUBLE TAP!
params:set(self.id .. '_' .. 'beat_start', x - 1)
params:set(self.id .. '_' .. 'beat_end', x - 1)
end
self.ui.slice_button_saved = nil
else
self.ui.slice_button_saved = x
end
else
self.ui.slice_button_saved = nil
end
if count == 2 then
params:set(self.id .. '_' .. 'beat_start', first - 1)
params:set(self.id .. '_' .. 'beat_end', second - 1)
end
else
if self.ui.slice_button_saved then
local count = 0
for _ in pairs(self.ui.slice_buttons_down) do
count = count + 1
end
if count ~= 1 then
self.ui.slice_button_saved = nil
end
end
self.ui.slice_buttons_down[x] = nil
end
end
if y == 2 and x <= self.loop_count then
if self.ui.shift_button == 1 then
if z == 1 and x ~= self.loop_index then
self:toggle_loop_enabled(x)
end
elseif z == 1 then
params:set(self.id .. '_' .. 'loop_index', x)
end
end
local c = 0
for _ in pairs(PROBABILITY_ORDER) do
c = c + 1
end
if x <= c and y > 3 and z == 1 then
local name = PROBABILITY_ORDER[x]
local value = (8 - y) / 4
params:set(self.id .. '_' .. name .. '_probability', value)
end
end
function Beets:drawGridUI(g, top_x, top_y)
if self.loop_count == 0 then
return
end
-- auto-advance
if params:get(self.id .. '_' .. 'auto_advance') == 2 then
g:led(top_x + 7, top_y + 2, 15)
else
g:led(top_x + 7, top_y + 2, 4)
end
-- shift
if self.ui.shift_button == 1 then
g:led(top_x + 0, top_y + 2, 15)
else
g:led(top_x + 0, top_y + 2, 4)
end
local mute_brightness = 4
if self.ui.mute_button == 1 then
mute_brightness = 15
else
if self.muted then
mute_brightness = 12
end
end
g:led(top_x + 7, top_y + 7, mute_brightness)
-- beat (0-based)
for i = 0, self.beat_count - 1 do
if self:slice_is_enabled(i) then
if i == self.played_index then
g:led(top_x + i, top_y, 15)
elseif i >= self.beat_start and i <= self.beat_end then
g:led(top_x + i, top_y, 6)
else
g:led(top_x + i, top_y, 3)
end
else
g:led(top_x + i, top_y, 1)
end
end
-- loop index (1-based)
for i = 0, math.min(7, self.loop_count - 1) do
if i == self.loop_index - 1 then
g:led(top_x + i, top_y + 1, 15)
elseif self:loop_at_index(i + 1).enabled == 1 then
g:led(top_x + i, top_y + 1, 3)
else
g:led(top_x + i, top_y + 1, 1)
end
end
local stripe_min = 5
local inter_stripe_diff = 1
for x, name in ipairs(PROBABILITY_ORDER) do
local value = self.probability[name]
range = 5
local scaled_value = value / 100 * range
local stripe_mod = inter_stripe_diff * (x % 2)
for i = 1, range do
local y = 8 - i
local brightness
if scaled_value > i then
brightness = 15 - stripe_mod
elseif scaled_value > i - 1 then
brightness = (15 - stripe_min - stripe_mod) * (scaled_value - (i - 1)) + stripe_mod + stripe_min
else
brightness = stripe_mod + stripe_min
end
g:led(top_x + x - 1, top_y + y, math.floor(brightness))
end
end
end
function Beets:drawDebugUI()
screen.clear()
screen.level(15)
for i, k in ipairs({'beatstep', 'index'}) do
screen.move(0, 7 * i)
screen.text(k .. ': ' .. self[k])
end
end
function Beets:drawPlaybackUI()
screen.clear()
screen.level(15)
if self.loop_count > 0 then
self:_drawCurrentLoopGrid {}
-- draw loop start/end
screen.level(6)
screen.move(
layout.left_margin + self.beat_start * layout.horiz_spacing,
layout.top_margin + layout.vert_spacing + 2
)
screen.line(
layout.left_margin + self.beat_start * layout.horiz_spacing,
layout.top_margin + layout.vert_spacing + 6
)
screen.line(
layout.left_margin + (self.beat_end + 1) * layout.horiz_spacing,
layout.top_margin + layout.vert_spacing + 6
)
screen.line(
layout.left_margin + (self.beat_end + 1) * layout.horiz_spacing,
layout.top_margin + layout.vert_spacing + 2
)
screen.stroke()
-- draw event indicators
screen.level(15)
screen.move(layout.left_margin + self.beat_count * layout.horiz_spacing + 30, layout.sidebar_top_margin)
screen.text(self.played_loop_index)
for y, e in ipairs(EVENT_ORDER) do
screen.move(
layout.left_margin + self.beat_count * layout.horiz_spacing + 30,
layout.sidebar_top_margin + layout.vert_spacing * y
)
if self.events[e] == 1 then
screen.level(15)
else
screen.level(1)
end
screen.text(e)
end
end
screen.level(15)
screen.move(layout.left_margin, 40)
screen.text(self.message)
screen.move(layout.left_margin, 50)
screen.text(self.status)
end
function Beets:drawEditingUI()
if self.loop_count > 0 then
self:_drawCurrentLoopGrid {
played_index = math.floor(self.editing_mode.cursor_location),
beatstep = math.floor(self.editing_mode.cursor_location)
}
end
screen.move(layout.left_margin, 50)
screen.text('EDIT MODE')
end
function Beets:drawUI()
screen.clear()
screen.level(15)
if self.debug then
self:drawDebugUI()
elseif self.editing then
self:drawEditingUI()
else
self:drawPlaybackUI()
end
screen.update()
end
function Beets:edit_mode_begin()
self.editing = true
self.enable_mutations = false
redraw()
end
function Beets:loop_at_index(index)
return self.loops_by_filename[self.loop_index_to_filename[index]]
end
function Beets:edit_mode_end()
self.editing = false
self.enable_mutations = true
local loop = self:loop_at_index(self.loop_index)
self:save_loop_info(loop)
redraw()
end
function Beets:enc(n, d)
if n == 1 then
self.editing_mode.cursor_location = (self.editing_mode.cursor_location + (d / 50.0)) % self.beat_count
redraw()
else
end
end
function Beets:on_render(start, i, s)
for i = 1, self.loop_count do
local loop = self:loop_at_index(i)
if loop.start == start then
loop.waveform_samples = s
end
end
end
function Beets:key(n, z)
if n == 2 and z == 0 then
local beat_types_index = math.floor(self.editing_mode.cursor_location) + 1
local loop = self:loop_at_index(self.loop_index)
if loop.beat_types[beat_types_index] == ' ' then
loop.beat_types[beat_types_index] = 'K'
elseif loop.beat_types[beat_types_index] == 'K' then
loop.beat_types[beat_types_index] = 'S'
elseif loop.beat_types[beat_types_index] == 'S' then
loop.beat_types[beat_types_index] = ' '
end
redraw()
else
print('Key ' .. n .. ' ' .. z)
end
end
return Beets