Skip to content
Permalink
master
Go to file
 
 
Cannot retrieve contributors at this time
executable file 1786 lines (1541 sloc) 44.5 KB
-- los angeles traffic
-- headlines ON in rain
-- 1.2.0
--
--
--
--
-- enc/arc 1: circle of 5ths
-- enc/arc 2: jump notes
-- enc 3: add/remove
-- key 2 (tap): major/minor
-- key 2 (hold) + enc 2:
-- select jump amount
-- key 3 (tap):
-- record pattern,
-- play pattern,
-- (hold) clear pattern
-- arc 3: env2 ADSR
-- arc 4: env3 ADSR
--
--
--
--
--
-- slow down
MusicUtil = require "musicutil"
local MollyThePoly = require "molly_the_poly/lib/molly_the_poly_engine"
local pattern_time = require 'pattern_time'
local Formatters = require "formatters"
local ControlSpec = require "controlspec"
engine.name = "MollyThePoly"
local options = {}
options.OUTPUT = {"Audio + MIDI", "Audio", "MIDI"}
options.STEP_LENGTH_NAMES = {"1 bar", "1/2", "1/3", "1/4", "1/6", "1/8", "1/12", "1/16", "1/24", "1/32", "1/48", "1/64"}
options.STEP_LENGTH_DIVIDERS = {1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64}
options.traffic_length_NAMES = {'normal','phasing', 'stationary', 'road rage'}
options.traffic_length_DIVIDERS = {1, 2, 3, 4}
options.OSC_WAVE_SHAPE = {"Triangle", "Saw", "Pulse"}
options.PW_MOD_SRC = {"LFO", "Env 1", "Manual"}
options.LP_FILTER_TYPE = {"-12 dB/oct", "-24 dB/oct"}
options.LP_FILTER_ENV = {"Env-1", "Env-2"}
options.LFO_WAVE_SHAPE = {"Sine", "Triangle", "Saw", "Square", "Random"}
options.self_driving_NAMES = {"off", "on"}
local syncOffset = 0
local triggerDuration
local gridDevice
local GRID_FRAMERATE = 30
local SCREEN_FRAMERATE = 15
local ARC_FRAMERATE = 20
local TRAIL_ANI_LENGTH = 6.0
local DOWN_ANI_LENGTH = 0.2
local gridDirty = true
local arcDirty = true
local gridLEDs = {}
local trails = {}
local downMarks = {}
local downKeys = {}
local removeAnimations = {}
local grid_w, grid_h = 16, 8
local notes = {}
local triggers = {}
local activeNotes = {}
local stepDuration
local stepsPerBar
local tonality = 'Natural Minor'
local rootNote = math.random(12)
local octave = rootNote + (4*12)
local masterScale
local gridScale = {}
local newGridScale = {}
local timeLast = util.time()
local currentScale
local stepsPerBar
local currentStep = 0
local bar = 0
local transposeAmount
local pivotAmount = 1
local clearBuffer
local progression = {}
progression.isplaying = false
local patternClearer
local prevTranspose = 60
local ar = arc.connect(1)
local midi_in_device
function pitch_quantizer(n)
local negative
if n < 0 then negative = true end
n = math.abs(n)
if n == 0 then return n end
if n == 1 then n = 1 end
if n == 2 then n = 1
elseif n == 03 then n = 2
elseif n == 04 then n = 2
elseif n == 05 then n = 3
elseif n == 06 then n = 3
elseif n == 07 then n = 4
elseif n == 08 then n = 4
elseif n == 09 then n = 5
elseif n == 10 then n = 6
elseif n == 11 then n = 6
elseif n == 12 then n = 7
elseif n == 13 then n = 7
elseif n == 14 then n = 1 + 7
elseif n == 15 then n = 2 + 7
elseif n == 16 then n = 2 + 7
elseif n == 17 then n = 3 + 7
elseif n == 18 then n = 3 + 7
elseif n == 19 then n = 4 + 7
elseif n == 20 then n = 4 + 7
elseif n == 21 then n = 5 + 7
elseif n == 22 then n = 6 + 7
elseif n == 23 then n = 6 + 7
elseif n == 24 then n = 7 + 7
end
if negative then n = -(n) end
return n
end
--for pattern
function get_bar_length()
if stepDuration == 1 then --1 bar
stepsPerBar = 1
elseif stepDuration == 2 then -- "1/2"
stepsPerBar = 2
elseif stepDuration == 3 then -- "1/3",
stepsPerBar = 3
elseif stepDuration == 4 then -- "1/4",
stepsPerBar = 4
elseif stepDuration == 5 then -- "1/6",
stepsPerBar = 6
elseif stepDuration == 6 then -- "1/8",
stepsPerBar = 8
elseif stepDuration == 7 then -- "1/12",
stepsPerBar = 12
elseif stepDuration == 8 then -- "1/16",
stepsPerBar = 16
elseif stepDuration == 9 then -- "1/24",
stepsPerBar = 24
elseif stepDuration == 10 then -- "1/32",
stepsPerBar = 32
elseif stepDuration == 11 then -- "1/48",
stepsPerBar = 48
elseif stepDuration == 12 then -- "1/64"}
stepsPerBar = 64
end
return stepsPerBar
end
function change_tonality()
local newTonality
if tonality == 'Natural Minor' then
newTonality = 'Major'
else
newTonality = 'Natural Minor'
end
change_scale(0, newTonality)
end
function background_change_tonality()
if progression.tonality == 'Natural Minor' then
progression.tonality = 'Major'
else
progression.tonality = 'Natural Minor'
end
background_change_scale(0, progression.tonality)
end
function set_octave(newOctave)
octave = rootNote + (newOctave * 12)
end
function capture_scale()
--print('capturing', MusicUtil.note_num_to_name(gridScale[1]))
progression.gridscale = {}
progression.masterscale = {}
progression.tonality = tonality
progression.rootnote = rootNote
progression.prevtranspose = prevTranspose
for k,v in pairs(gridScale) do
progression.gridscale[k] = v
end
for k,v in pairs(masterScale) do
progression.masterscale[k] = v
end
end
function change_scale(semitones,scaleType,clockwisemotion)
local incomingScale = {}
local notesToKeep = {}
local incomingGridScale = {}
local newRoot
clockwisemotion = clockwisemotion or 1
if semitones == 0 and scaleType ~= tonality then
tonality = scaleType
end
newRoot = (rootNote + semitones) % 12
incomingScale = MusicUtil.generate_scale_of_length(newRoot, scaleType, 128)
--get tone map of new pitches
for k,v in pairs(masterScale) do
for n_k, n_v in pairs(incomingScale) do
if n_v == v then
notesToKeep[k] = v
break
elseif n_v ~= v then
if n_v - 1 == v then
notesToKeep[k] = v + 1
break
end
else
if n_v + 1 == v then
notesToKeep[k] = v - 1
break
end
end
end
end
--map scale to grid
for k,v in pairs(notesToKeep) do
if #incomingGridScale < 16 then
if clockwisemotion < 0 then
if v >= (gridScale[1] - 1) then
incomingGridScale[#incomingGridScale + 1] = v
end
elseif clockwisemotion > 0 then
if v >= (gridScale[1]) then
incomingGridScale[#incomingGridScale + 1] = v
end
end
elseif #incomingGridScale == 16 then
break
end
end
--detect duplicates
for k,v in pairs(incomingGridScale) do
if incomingGridScale [k] == incomingGridScale[k+1] then
for a, b in pairs(incomingScale) do
if v == b then
incomingGridScale[k+1] = incomingScale[a+1]
break
end
end
end
end
gridScale = incomingGridScale
rootNote = newRoot
masterScale = incomingScale
screenDirty = true
arcDirty = true
end
function background_change_scale(semitones,scaleType,clockwisemotion)
local incomingScale = {}
local notesToKeep = {}
local incomingGridScale = {}
local newRoot
clockwisemotion = clockwisemotion or 1
if semitones == 0 and scaleType ~= tonality then
tonality = scaleType
end
newRoot = (progression.rootnote + semitones) % 12
incomingScale = MusicUtil.generate_scale_of_length(newRoot, scaleType, 128)
--get tone map of new pitches
for k,v in pairs(progression.masterscale) do
for n_k, n_v in pairs(incomingScale) do
if n_v == v then
notesToKeep[k] = v
break
elseif n_v ~= v then
if n_v - 1 == v then
notesToKeep[k] = v + 1
break
end
else
if n_v + 1 == v then
notesToKeep[k] = v - 1
break
end
end
end
end
--map scale to grid
for k,v in pairs(notesToKeep) do
if #incomingGridScale < 16 then
if clockwisemotion < 0 then
if v >= (gridScale[1] - 1) then
incomingGridScale[#incomingGridScale + 1] = v
end
elseif clockwisemotion > 0 then
if v >= (gridScale[1]) then
incomingGridScale[#incomingGridScale + 1] = v
end
end
elseif #incomingGridScale == 16 then
break
end
end
--detect duplicates
for k,v in pairs(incomingGridScale) do
if incomingGridScale [k] == incomingGridScale[k+1] then
for a, b in pairs(incomingScale) do
if v == b then
incomingGridScale[k+1] = incomingScale[a+1]
break
end
end
end
end
progression.gridscale = incomingGridScale
progression.rootnote = newRoot
progression.masterscale = incomingScale
end
function pivot_within_scale(pivot)
if pivot == 0 then return end
local counter = 0
for mk, mv in pairs(masterScale) do
if mv == gridScale[1] then
for gk,gv in pairs(gridScale) do
if not masterScale[mk + pivot + counter] then return end
gridScale[gk] = masterScale[mk + pivot + counter]
counter = counter + 1
end
break
end
end
end
function background_pivot_within_scale(pivot)
local counter = 1
for mk, mv in pairs(progression.masterscale) do
if mv >= progression.gridscale[1] then
if not progression.masterscale[mk + pivot] then return end
progression.gridscale[counter] = progression.masterscale[mk + pivot]
counter = counter + 1
end
if counter == 17 then
counter = 1
break
end
end
end
function play_progression(e)
if pat.step <= 1 then
prevTranspose = progression.prevtranspose
for k,v in pairs(progression.gridscale) do
gridScale[k] = v
end
--print('resetting to captured', MusicUtil.note_num_to_name(gridScale[1]))
for k,v in pairs(progression.masterscale) do
masterScale[k] = v
end
tonality = progression.tonality
rootNote = progression.rootnote
screenDirty = true
arcDirty = true
end
if e then
if e.id == 'key' then
key_event(e.number, e.state)
elseif e.id == 'enc' then
enc_event(e.number, e.state)
elseif e.id == 'midi' then
midi_transpose_event(e.number)
elseif e.id == 'arc' then
arc_event(e.number, e.state)
end
end
end
function enc_event(n, delta)
--enc 1
if n == 1 then
if delta > 0 then
transposeAmount = 7
elseif delta < 0 then
transposeAmount = -7
end
if util.time() - timeLast > .1 then
change_scale(transposeAmount, tonality,delta)
end
--encoder 2
elseif n == 2 then
local prevPivot = pivotAmount
if delta > 0 then
pivotAmount = math.abs(pivotAmount)
elseif delta < 0 then
pivotAmount = pivotAmount * -1
end
if util.time() - timeLast > .1 then pivot_within_scale(pivotAmount) end
pivotAmount = prevPivot
if pivotAmount <= 0 then pivotAmount = 7 end
--encoder 3
elseif n == 3 then
if delta > 0 then
elseif delta < 0 then
end
end
timeLast = util.time()
end
function key_event(n, z)
if z == 1 then
clearBuffer = util.time()
end
--key 1
if n == 1 then
elseif n == 2 then
if z == 0 then
if util.time() - clearBuffer < .5 then
change_tonality()
end
end
--key 3
elseif n == 3 then
end
end
function midi_transpose_event(data)
local note
if data ~= prevTranspose then
note = pitch_quantizer(data - prevTranspose)
if note then pivot_within_scale(note) end
end
prevTranspose = data
end
function arc_event(n, delta)
if n == 1 then
if delta > 0 then
transposeAmount = 7
elseif delta < 0 then
transposeAmount = -7
end
if util.time() - timeLast > .1 then
change_scale(transposeAmount, tonality,delta)
end
--encoder 2
elseif n == 2 then
local prevPivot = pivotAmount
if arc2Check == nil then arc2Check = 0 end
if delta > 0 then pivotAmount = math.abs(pivotAmount)
elseif delta < 0 then pivotAmount = pivotAmount * -1 end
if util.time() - timeLast > .1 or arc2Check > 10 then
arc2Check = 0
pivot_within_scale(pivotAmount)
else
arc2Check = arc2Check + math.abs(delta)
end
pivotAmount = prevPivot
end
timeLast = util.time()
arcDirty = true
screenDirty = true
end
function midi_event(data)
--print('what')
local d = midi.to_msg(data)
local note
local e = {}
if d.type == "note_on" then
e.id = 'midi'
e.number = d.note
pat:watch(e)
if d.note ~= prevTranspose then
note = pitch_quantizer(d.note - prevTranspose)
if note then pivot_within_scale(note) end
end
prevTranspose = d.note
end
end
function enc(n, delta)
local e = {}
e.id = 'enc'
e.number = n
e.state = delta
pat:watch(e)
--enc 1
if n == 1 then
if delta > 0 then transposeAmount = 7
elseif delta < 0 then transposeAmount = -7 end
if util.time() - timeLast > .1 then
if not progression.isplaying then
change_scale(transposeAmount, tonality,delta)
else
background_change_scale(transposeAmount, tonality,delta)
end
end
--encoder 2
elseif n == 2 then
if not shiftA then
local prevPivot = pivotAmount
if delta > 0 then pivotAmount = math.abs(pivotAmount)
elseif delta < 0 then pivotAmount = pivotAmount * -1 end
if util.time() - timeLast > .1 then
if not progression.isplaying then
pivot_within_scale(pivotAmount)
else
background_pivot_within_scale(pivotAmount)
end
end
pivotAmount = prevPivot
elseif shiftA then
--prevPivot = pivotAmount
if delta > 0 then
if util.time() - timeLast > .1 then pivotAmount = pivotAmount%7+delta end
elseif delta < 0 then
if util.time() - timeLast > .1 then pivotAmount = pivotAmount%9+delta end
end
if pivotAmount <= 0 then pivotAmount = 7 end
end
--encoder 3
elseif n == 3 then
if not shiftA then
if delta > 0 then
add_random()
elseif delta < 0 then
remove_last()
end
else
local x = params:get("traffic_length")
if util.time() - timeLast > .1 then
x = x + delta
if x > 4 then x = 1
elseif x < (-2) then x = (-1) end
params:set("traffic_length", x)
end
end
end
timeLast = util.time()
end
function key(n, z)
if z == 1 then
clearBuffer = util.time()
end
--key 2
if n == 2 then
local e = {}
e.id = 'key'
e.number = n
e.state = z
--print(e.id, e.number, e.state)
pat:watch(e)
if z == 1 then shiftA = true end
if z == 0 then
shiftA = false
if util.time() - clearBuffer < .5 then
if not progression.isplaying then
change_tonality()
arcDirty = true
else
background_change_tonality()
end
end
end
--key 3
elseif n == 3 then
if z == 1 then
patternClearer = true
if pat.rec == 0 then
if not progression.isplaying then
capture_scale()
pat:stop()
pat:clear()
pat:rec_start()
end
elseif pat.rec == 1 then
pat:rec_stop()
pat:start()
if pat.count > 0 then progression.isplaying = true end
end
elseif z == 0 then
patternClearer = false
if pat.rec == 1 then
local e = {}
e.id = 'key'
e.number = n
e.state = z
--print(e.id, e.number, e.state)
pat:watch(e)
end
if util.time() - clearBuffer > 1 then
pat:clear()
print('cleared')
progression.isplaying = false
end
end
end
end
local function note_on(note_num)
local min_vel, max_vel = params:get("min_velocity"), params:get("max_velocity")
if min_vel > max_vel then
max_vel = min_vel
end
local note_midi_vel = math.random(min_vel, max_vel)
-- print("note_on", note_num, note_midi_vel)
-- Audio engine out
if params:get("output") == 1 or params:get("output") == 3 then
engine.noteOn(note_num, MusicUtil.note_num_to_freq(note_num), note_midi_vel / 127)
end
-- MIDI out
if (params:get("output") == 2 or params:get("output") == 3) then
midi_out_device:note_on(note_num, note_midi_vel, midi_out_channel)
end
end
function note_off(noteNum)
if params:get("output") == 1 or params:get("output") == 3 then
engine.noteOff(noteNum)
end
if (params:get("output") == 2 or params:get("output") == 3) then
midi_out_device:note_off(noteNum, nil, midi_out_channel)
end
end
function all_notes_kill()
engine.noteKillAll()
if (params:get("output") == 2 or params:get("output") == 3) then
for _, a in pairs(activeNotes) do
midi_out_device:note_off(a, 96, midi_out_channel)
end
end
activeNotes = {}
end
function add_note(position, head, length, direction)
length = length or 1
direction = direction or 1
local note = {position = position, head = head, length = length, advance_countdown = length, direction = direction, active = false}
table.insert(notes, note)
gridDirty = true
end
function add_trigger(position, head, length, direction)
local triggerDuration
if params:get("traffic_length") == 1 then
length = length or 1
direction = direction or 1
elseif params:get("traffic_length") == 2 then
length = (length or 1) + 2
direction = direction or 1
elseif params:get("traffic_length") == 4 then
length = length or 1
direction = -1 * (direction or 1)
end
local trigger = {position = position, head = head, length = length, advance_countdown = length, direction = direction, active = false}
table.insert(triggers, trigger)
gridDirty = true
end
function remove_note(position, silent)
local note
if position then
for k, v in pairs(notes) do
if v.position == position then
note= table.remove(notes, k)
break
end
end
else
note = table.remove(notes)
end
if note and not silent then
gridDirty = true
end
end
function remove_trigger(position, silent)
local trigger
if position then
for k,v in pairs(triggers) do
if v.position == position then
trigger = table.remove(triggers, k)
break
end
end
else
trigger = table.remove(triggers)
end
if trigger and not silent then
gridDirty = true
end
end
function add_random()
if #notes >= grid_w and #triggers >= grid_h then return end
if math.random() >= .5 then
local availablePositions = {}
for i = 1, grid_w do
local available = true
for _, vn in pairs(notes) do
if vn.position == i then
available = false
break
end
end
if available then
table.insert(availablePositions, i)
end
end
if #availablePositions > 0 then
local length = util.round(math.pow(math.random(), 4) * (grid_h - 2) + 1)
add_note(availablePositions[math.random(#availablePositions)],
math.random(grid_h),
length,
math.random() >= 0.5 and 1 or -1)
end
else
local availablePositions = {}
for i = 1, grid_h do
local available = true
for _, vt in pairs(triggers) do
if vt.position == i then
available = false
break
end
end
if available then
table.insert(availablePositions, i)
end
end
if #availablePositions > 0 then
local length = util.round(math.pow(math.random(), 4) * (grid_w - 2) + 1)
add_trigger(availablePositions[math.random(#availablePositions)], math.random(grid_w), length, (math.random() >= 0.5 and 1 or -1), true)
end
end
end
function remove_last()
if #notes == 0 and #triggers == 0 then return end
if math.random() >= .5 then
remove_note(nil, true)
else
remove_trigger(nil, true)
end
end
function advance_step()
currentStep = currentStep + 1
if currentStep >= stepsPerBar then
bar = bar + 1
currentStep = 0
if params:get("self_driving") == 2 then
changer = math.random(1,3)
if changer == 1 then
pivot_within_scale(math.random(-2,2),bar)
elseif changer == 2 then
change_scale(math.random(-1,1) * 5, tonality)
end
end
end
if gridDevice then
grid_w = gridDevice.cols
grid_h = gridDevice.rows
if grid_w ~= 8 and grid_w ~= 16 then grid_w = 16 end
if grid_h ~= 8 and grid_h ~= 16 then grid_h = 8 end
end
for _, n in pairs(notes) do
n.advance_countdown = n.advance_countdown - 1
if n.advance_countdown == 0 then
n.advance_countdown = n.length
if n.direction > 0 then n.head = n.head % params:get("pattern_height") + 1
else n.head = (n.head + params:get("pattern_height") - 2) % params:get("pattern_height") + 1 end
end
n.active = false
end
local activeNotesThisStep = {}
for _, t in pairs(triggers) do
-- Progress
if params:get("traffic_length") == 3 then
t.advance_countdown = t.advance_countdown
elseif params:get("traffic_length") == 4 then
t.direction = t.direction * -1
t.advance_countdown = t.advance_countdown - 1
else
t.advance_countdown = t.advance_countdown - 1
end
if t.advance_countdown == 0 then
t.advance_countdown = t.length
if t.direction > 0 then t.head = t.head % params:get("pattern_width") + 1
else t.head = (t.head + params:get("pattern_width") - 2) % params:get("pattern_width") + 1 end
end
t.active = false
-- Check for intersections and generate trails
local tx
for ti = 0, t.length - 1 do
tx = t.head + (ti * t.direction * -1)
tx = (tx - 1) % params:get("pattern_width") + 1
if tx <= grid_w then trails[tx][t.position] = TRAIL_ANI_LENGTH end
for _, n in pairs(notes) do
local ny
for ni = 0, n.length - 1 do
ny = n.head + (ni * n.direction * -1)
ny = (ny - 1) % params:get("pattern_height") + 1
if ny <= grid_h then trails[n.position][ny] = TRAIL_ANI_LENGTH end
if tx == n.position and t.position == ny then
if not n.active then
table.insert(activeNotesThisStep, gridScale[n.position])
end
n.active = true
t.active = true
break
end
end
end
end
end
-- Generate trails for notes if need be
if #triggers == 0 then
for _, n in pairs(notes) do
local ny
for ni = 0, n.length - 1 do
ny = n.head + (ni * n.direction * -1)
ny = (ny - 1) % params:get("pattern_height") + 1
if ny <= grid_h then trails[n.position][ny] = TRAIL_ANI_LENGTH end
end
end
end
-- Work out which need noteOffs
for i = #activeNotes, 1, -1 do
local still_active = false
for sk, sa in pairs(activeNotesThisStep) do
if sa == activeNotes[i] then
still_active = true
table.remove(activeNotesThisStep, sk)
break
end
end
if not still_active then
note_off(activeNotes[i])
table.remove(activeNotes, i)
end
end
-- Add remaining, the new notes
for _, sa in pairs(activeNotesThisStep) do
if #activeNotes < params:get("max_active_notes") then
note_on(sa)
table.insert(activeNotes, sa)
end
end
screenDirty = true
arcDirty = true
gridDirty = true
end
local function grid_update()
if #downMarks > 0 or #removeAnimations > 0 then gridDirty = true end
local time_increment = 1 / GRID_FRAMERATE
-- Trails
for x = 1, grid_w do
for y = 1, grid_h do
trails[x][y] = util.clamp(trails[x][y] - time_increment, 0, TRAIL_ANI_LENGTH)
if trails[x][y] > 0 then gridDirty = true end
end
end
-- Down marks
for i = #downMarks, 1, -1 do
if not downMarks[i].active then
downMarks[i].time_remaining = downMarks[i].time_remaining - time_increment
if downMarks[i].time_remaining <= 0 then
table.remove(downMarks, i)
end
end
end
-- Remove animations
for i = #removeAnimations, 1, -1 do
removeAnimations[i].time_remaining = removeAnimations[i].time_remaining - time_increment
if removeAnimations[i].time_remaining <= 0 then
table.remove(removeAnimations, i)
end
end
end
local function stop()
all_notes_kill()
end
local function grid_update()
if #downMarks > 0 then gridDirty = true end
local time_increment = 1 / GRID_FRAMERATE
-- Trails
for x = 1, grid_w do
for y = 1, grid_h do
trails[x][y] = util.clamp(trails[x][y] - time_increment, 0, TRAIL_ANI_LENGTH)
if trails[x][y] > 0 then gridDirty = true end
end
end
-- Down marks
for i = #downMarks, 1, -1 do
if not downMarks[i].active then
downMarks[i].time_remaining = downMarks[i].time_remaining - time_increment
if downMarks[i].time_remaining <= 0 then
table.remove(downMarks, i)
end
end
end
end
function pulse()
while true do
clock.sync(1/(stepsPerBar/4))
if syncOffset == 0 then
advance_step()
else if syncOffset > 0 then
syncOffset = syncOffset - 1
else
syncOffset = 0
end
end
end
end
function init()
midi_in_device = midi.connect(1)
midi_in_device.event = midi_event
masterScale = MusicUtil.generate_scale_of_length(rootNote, tonality, 128)
--set first scale
for _, v in pairs(masterScale) do
if v >= octave then
gridScale[#gridScale + 1] = v
end
if #gridScale > 15 then break end
end
for x = 1, 16 do
gridLEDs[x] = {}
trails[x] = {}
for y = 1, 16 do
gridLEDs[x][y] = 0
trails[x][y] = 0
end
end
capture_scale()
gridDevice = grid.connect(1)
gridDevice.key = gridKey
midi_out_device = midi.connect(1)
midi_out_device.event = function() end
local screen_refresh_metro = metro.init()
screen_refresh_metro.event = function()
if screenDirty then
screenDirty = false
redraw()
end
end
local grid_redraw_metro = metro.init()
grid_redraw_metro.event = function()
grid_update()
if gridDirty and gridDevice.device then
gridDirty = false
grid_redraw()
end
end
local arc_redraw_metro = metro.init()
arc_redraw_metro.event = function()
if arcDirty then
arcDirty = false
arc_redraw()
end
end
-- Add params
params:add{type = "option",
id = "step_length",
name = "Step Length",
options = options.STEP_LENGTH_NAMES,
default = 8,
action = function(value)
stepDuration = value
stepsPerBar = get_bar_length()
--print(stepDuration,stepsPerBar)
-- beat_clock.steps_per_beat = options.STEP_LENGTH_DIVIDERS[value] / 4
-- beat_clock:bpm_change(beat_clock.bpm)
end}
params:add{type = "trigger",
id = "sync_offset",
name = "Sync offset",
action = function() syncOffset = 1 end}
params:add{type = "option",
id = "traffic_length",
name = "Traffic",
options = options.traffic_length_NAMES,
default = 1,
action = function(value)
triggerDuration = value
end}
params:add_separator("engine params")
-- Engine params
--
params:add_group("oscillator",11)
params:add{type = "option", id = "osc_wave_shape", name = "Osc Wave Shape",
options = options.OSC_WAVE_SHAPE,
default = 3,
action = function(value) engine.oscWaveShape(value - 1) end}
params:add{type = "control", id = "pulse_width_mod",
name = "Pulse Width Mod",
controlspec = MollyThePoly.specs.PW_MOD,
action = engine.pwMod}
params:add{type = "option",
id = "pulse_width_mod_src",
name = "Pulse Width Mod Src",
options = options.PW_MOD_SRC,
action = function(value) engine.pwModSource(value - 1) end}
params:add{type = "control",
id = "freq_mod_lfo",
name = "Frequency Mod (LFO)",
controlspec = MollyThePoly.specs.FREQ_MOD_LFO,
action = engine.freqModLfo}
params:add{type = "control",
id = "freq_mod_env",
name = "Frequency Mod (Env-1)",
controlspec = MollyThePoly.specs.FREQ_MOD_ENV,
action = engine.freqModEnv}
params:add{type = "control",
id = "glide",
name = "Glide",
controlspec = MollyThePoly.specs.GLIDE,
formatter = Formatters.format_secs,
action = engine.glide}
params:add_separator()
params:add{type = "control",
id = "main_osc_level",
name = "Main Osc Level",
controlspec = MollyThePoly.specs.MAIN_OSC_LEVEL,
action = engine.mainOscLevel}
params:add{type = "control",
id = "sub_osc_level",
name = "Sub Osc Level",
controlspec = MollyThePoly.specs.SUB_OSC_LEVEL,
action = engine.subOscLevel}
params:add{type = "control",
id = "sub_osc_detune",
name = "Sub Osc Detune",
controlspec = MollyThePoly.specs.SUB_OSC_DETUNE,
action = engine.subOscDetune}
params:add{type = "control",
id = "noise_level",
name = "Noise Level",
controlspec = MollyThePoly.specs.NOISE_LEVEL,
action = engine.noiseLevel}
params:add_group("filter",8)
params:add{type = "control",
id = "hp_filter_cutoff",
name = "HP Filter Cutoff",
controlspec = MollyThePoly.specs.HP_FILTER_CUTOFF,
formatter = Formatters.format_freq,
action = engine.hpFilterCutoff}
params:add{type = "control",
id = "lp_filter_cutoff",
name = "LP Filter Cutoff",
controlspec = MollyThePoly.specs.LP_FILTER_CUTOFF,
formatter = Formatters.format_freq,
action = engine.lpFilterCutoff}
params:add{type = "control",
id = "lp_filter_resonance",
name = "LP Filter Resonance",
controlspec = MollyThePoly.specs.LP_FILTER_RESONANCE,
action = engine.lpFilterResonance}
params:add{type = "option",
id = "lp_filter_type",
name = "LP Filter Type",
options = options.LP_FILTER_TYPE,
default = 2,
action = function(value) engine.lpFilterType(value - 1) end}
params:add{type = "option",
id = "lp_filter_env",
name = "LP Filter Env",
options = options.LP_FILTER_ENV,
action = function(value) engine.lpFilterCutoffEnvSelect(value - 1) end}
params:add{type = "control",
id = "lp_filter_mod_env",
name = "LP Filter Mod (Env)",
controlspec = MollyThePoly.specs.LP_FILTER_CUTOFF_MOD_ENV,
action = engine.lpFilterCutoffModEnv}
params:add{type = "control",
id = "lp_filter_mod_lfo",
name = "LP Filter Mod (LFO)",
controlspec = MollyThePoly.specs.LP_FILTER_CUTOFF_MOD_LFO,
action = engine.lpFilterCutoffModLfo}
params:add{type = "control",
id = "lp_filter_tracking",
name = "LP Filter Tracking",
controlspec = MollyThePoly.specs.LP_FILTER_TRACKING,
formatter = format_ratio_to_one,
action = engine.lpFilterTracking}
params:add_group("LFO", 3)
params:add{type = "control",
id = "lfo_freq",
name = "LFO Frequency",
controlspec = MollyThePoly.specs.LFO_FREQ,
formatter = Formatters.format_freq,
action = engine.lfoFreq}
params:add{type = "option",
id = "lfo_wave_shape",
name = "LFO Wave Shape",
options = options.LFO_WAVE_SHAPE,
action = function(value) engine.lfoWaveShape(value - 1) end}
params:add{type = "control",
id = "lfo_fade",
name = "LFO Fade",
controlspec = MollyThePoly.specs.LFO_FADE,
formatter = format_fade,
action = function(value)
if value < 0 then value = MollyThePoly.specs.LFO_FADE.minval - 0.00001 + math.abs(value) end
engine.lfoFade(value)
end}
params:add_group("envelopes/amp", 12)
params:add{type = "control",
id = "env_1_attack",
name = "Env-1 Attack",
controlspec = MollyThePoly.specs.ENV_ATTACK,
formatter = Formatters.format_secs,
action = engine.env1Attack}
params:add{type = "control",
id = "env_1_decay",
name = "Env-1 Decay",
controlspec = MollyThePoly.specs.ENV_DECAY,
formatter = Formatters.format_secs,
action = engine.env1Decay}
params:add{type = "control",
id = "env_1_sustain",
name = "Env-1 Sustain",
controlspec = MollyThePoly.specs.ENV_SUSTAIN,
action = engine.env1Sustain}
params:add{type = "control",
id = "env_1_release",
name = "Env-1 Release",
controlspec = MollyThePoly.specs.ENV_RELEASE,
formatter = Formatters.format_secs,
action = engine.env1Release}
params:add_separator()
params:add{type = "control",
id = "env_2_attack",
name = "Env-2 Attack",
controlspec = MollyThePoly.specs.ENV_ATTACK,
formatter = Formatters.format_secs,
action = engine.env2Attack}
params:add{type = "control",
id = "env_2_decay",
name = "Env-2 Decay",
controlspec = MollyThePoly.specs.ENV_DECAY,
formatter = Formatters.format_secs,
action = engine.env2Decay}
params:add{type = "control",
id = "env_2_sustain",
name = "Env-2 Sustain",
controlspec = MollyThePoly.specs.ENV_SUSTAIN,
action = engine.env2Sustain}
params:add{type = "control",
id = "env_2_release",
name = "Env-2 Release",
controlspec = MollyThePoly.specs.ENV_RELEASE,
formatter = Formatters.format_secs,
action = engine.env2Release}
params:add_separator()
params:add{type = "control",
id = "amp",
name = "Amp",
controlspec = MollyThePoly.specs.AMP,
action = engine.amp}
params:add{type = "control",
id = "amp_mod",
name = "Amp Mod (LFO)",
controlspec = MollyThePoly.specs.AMP_MOD,
action = engine.ampMod}
params:add_group("effects/randomization", 10)
params:add{type = "control",
id = "ring_mod_freq",
name = "Ring Mod Frequency",
controlspec = ControlSpec.new(.1, 300, "exp", 0, 50, "Hz"),
formatter = Formatters.format_freq,
action = engine.ringModFreq}
params:add{type = "control",
id = "ring_mod_fade",
name = "Ring Mod Fade",
controlspec = MollyThePoly.specs.RING_MOD_FADE,
formatter = format_fade,
action = function(value)
if value < 0 then value = MollyThePoly.specs.RING_MOD_FADE.minval - 0.00001 + math.abs(value) end
engine.ringModFade(value)
end}
params:add{type = "control",
id = "ring_mod_mix",
name = "Ring Mod Mix",
controlspec = MollyThePoly.specs.RING_MOD_MIX,
action = engine.ringModMix}
params:add{type = "control",
id = "chorus_mix",
name = "Chorus Mix",
controlspec = MollyThePoly.specs.CHORUS_MIX,
action = engine.chorusMix}
params:add_separator()
params:bang()
params:add{type = "trigger",
id = "create_lead",
name = "Create Lead",
action = function() MollyThePoly.randomize_params("lead") end}
params:add{type = "trigger",
id = "create_pad",
name = "Create Pad",
action = function() MollyThePoly.randomize_params("pad") end}
params:add{type = "trigger",
id = "create_percussion",
name = "Create Percussion",
action = function() MollyThePoly.randomize_params("percussion") end}
params:add_separator()
params:add{type = "option",
id = "self_driving",
name = "Self Driving",
options = options.self_driving_NAMES,
default = 1,
action = function(value)
end}
params:add_separator()
params:add_group("output options", 11)
params:add{type = "number", id = "gridDevice", name = "Grid Device", min = 1, max = 4, default = 1,
action = function(value)
gridDevice:all(0)
gridDevice:refresh()
gridDevice.key = nil
gridDevice = grid.connect(value)
gridDevice.key = gridKey
end}
params:add{type = "option", id = "output", name = "Output", options = options.OUTPUT, action = all_notes_kill}
params:add{type = "number", id = "midi_device", name = "MIDI Device", min = 1, max = 4, default = 1, action = function(value)
midi_in_device.event = nil
midi_in_device = midi.connect(value)
midi_in_device.event = midi_event
end}
params:add{type = "number", id = "midi_out_device", name = "MIDI Out Device", min = 1, max = 4, default = 1,
action = function(value)
midi_out_device = midi.connect(value)
end}
params:add{type = "number", id = "midi_out_channel", name = "MIDI Out Channel", min = 1, max = 16, default = 1,
action = function(value)
all_notes_kill()
midi_out_channel = value
end}
params:add{type = "number", id = "max_active_notes", name = "Max Active Notes", min = 1, max = 10, default = 10}
midi_out_channel = params:get("midi_out_channel")
params:add_separator()
params:add{type = "number", id = "pattern_width", name = "Pattern Width", min = 4, max = 64, default = 16,
action = function()
gridDirty = true
end}
params:add{type = "number", id = "pattern_height", name = "Pattern Height", min = 4, max = 64, default = 8,
action = function()
gridDirty = true
end}
params:add{type = "number", id = "min_velocity", name = "Min Velocity", min = 1, max = 127, default = 80}
params:add{type = "number", id = "max_velocity", name = "Max Velocity", min = 1, max = 127, default = 100}
--metros
grid_redraw_metro:start(1 / GRID_FRAMERATE)
screen_refresh_metro:start(1 / SCREEN_FRAMERATE)
arc_redraw_metro:start(1 / ARC_FRAMERATE)
clock.run(pulse)
pat = pattern_time.new()
pat.process = play_progression
params:read("/home/we/dust/data/traffic/traffic.pset")
params:bang()
end
function gridKey(x, y, z)
if z == 1 then
-- Is there a relevant down mark?
local relevantDownMark = nil
for k, v in pairs(downMarks) do
-- Re-activate fading down mark
if v.x == x and v.y == y then
v.active = true
v.time_remaining = DOWN_ANI_LENGTH
relevantDownMark = v
break
-- Note
elseif v.x == x and v.active then
local threeKeys = false
for _, kv in pairs(downKeys) do
if kv.x == x then
threeKeys = true
break
end
end
if threeKeys then
remove_note(x, true, false)
else
remove_note(x, false, true)
add_note(x, v.y, math.abs(v.y - y), (y < v.y and 1 or -1), false)
end
relevantDownMark = v
-- Trigger
elseif v.y == y and v.active then
local threeKeys = false
for _, kv in pairs(downKeys) do
if kv.y == y then
threeKeys = true
break
end
end
if threeKeys then
remove_trigger(y, true, false)
else
remove_trigger(y, false, true)
add_trigger(y, v.x, math.abs(v.x - x), (x < v.x and 1 or -1), false)
end
relevantDownMark = v
end
end
-- Make it the down mark
if relevantDownMark then
if relevantDownMark.x ~= x or relevantDownMark.y ~= y then
table.insert(downKeys, {x = x, y = y})
end
else
table.insert(downMarks, {active = true, x = x, y = y, time_remaining = DOWN_ANI_LENGTH})
end
else
for _, v in pairs(downMarks) do
if v.x == x and v.y == y then
v.active = false
break
end
end
for k, v in pairs(downKeys) do
if v.x == x and v.y == y then
table.remove(downKeys, k)
break
end
end
end
gridDirty = true
end
function ar.delta(n, delta)
local e = {}
e.id = 'arc'
e.number = n
e.state = delta
pat:watch(e)
if n == 1 then
if arc1Check == nil then arc1Check = 0 end
if delta > 0 then transposeAmount = 7
elseif delta < 0 then transposeAmount = -7 end
if util.time() - timeLast > .5 or arc1Check > 100 then
arc1Check = 0
if not progression.isplaying then change_scale(transposeAmount, tonality)
else background_change_scale(transposeAmount, tonality) end
else
arc1Check = arc1Check + math.abs(delta)
end
--encoder 2
elseif n == 2 then
local prevPivot = pivotAmount
if arc2Check == nil then arc2Check = 0 end
if delta > 0 then pivotAmount = math.abs(pivotAmount)
elseif delta < 0 then pivotAmount = pivotAmount * -1 end
if util.time() - timeLast > .2 or arc2Check > 40 then
arc2Check = 0
if not progression.isplaying then pivot_within_scale(pivotAmount) else background_pivot_within_scale(pivotAmount) end
else
arc2Check = arc2Check + math.abs(delta)
end
pivotAmount = prevPivot
elseif n == 3 then
local changeAmount
changeAmount = (math.random(8,12)/10) * (delta/30)
if params:get("env_2_decay") > 2 then
params:delta("env_2_attack", changeAmount) end
if params:get("env_2_attack") < 0.1 then
params:delta("env_2_sustain", changeAmount)
elseif params:get("env_2_attack") > 4 then
params:delta("env_2_sustain", changeAmount) end
if params:get("env_2_attack") < 0.1 then
params:delta("env_2_decay", changeAmount*2)
else params:delta("env_2_decay", changeAmount) end
if params:get("env_2_attack") > 0.1 then
params:delta("env_2_release", changeAmount)
end
changeAmount = nil
elseif n == 4 then
local changeAmount
changeAmount = (math.random(8,12)/10) * (delta/30)
params:delta("lp_filter_cutoff", changeAmount)
params:delta("lp_filter_resonance", -changeAmount)
if params:get('lp_filter_mod_env') >= 0 then params:delta("lp_filter_mod_env", (changeAmount * -1)) end
if params:get("env_1_decay") > 1 then
params:delta("env_1_attack", changeAmount/2)
end
if params:get("env_1_decay") > 1 then
params:delta("env_1_sustain", changeAmount/2)
end
if params:get("env_1_decay") > .1 then params:delta("env_1_decay", changeAmount) end
if params:get("lp_filter_cutoff") < 100 then
params:delta("lp_filter_tracking", -1 * changeAmount/2)
end
changeAmount = nil
end
timeLast = util.time()
arcDirty = true
end
function arc_redraw()
ar:all(0)
for a = 1,64 do
local note = MusicUtil.note_num_to_name(a-1)
if string.find(note, MusicUtil.note_num_to_name(rootNote)) then
if string.len(note) == string.len(MusicUtil.note_num_to_name(rootNote)) then
ar:led(1,a,10) end
elseif string.find(note,"%#") then
ar:led(1,a,0)
elseif string.find(note,"C") then
ar:led(1,a,5)
else
ar:led(1,a,3)
end
for k,v in pairs(masterScale) do
if (a-1)+octave == v then
ar:led(2,a,5)
end
end
for k,v in pairs(gridScale) do
if (a-1)+octave == v then
ar:led(2,a,9)
end
end
end
ar:refresh()
end
function grid_redraw()
local DOWN_BRIGHTNESS = 1
local TRAIL_BRIGHTNESS = 1
local OUTSIDE_BRIGHTNESS = 1
local INACTIVE_BRIGHTNESS = 2
local ACTIVE_BRIGHTNESS = 4
local brightness
-- Draw trails
for x = 1, 16 do
for y = 1, 16 do
if trails[x][y] then gridLEDs[x][y] = util.round(util.linlin(0, TRAIL_ANI_LENGTH, 0, TRAIL_BRIGHTNESS, trails[x][y]))
else gridLEDs[x][y] = 0 end
if (x > params:get("pattern_width") or y > params:get("pattern_height")) and gridLEDs[x][y] < OUTSIDE_BRIGHTNESS then gridLEDs[x][y] = OUTSIDE_BRIGHTNESS end
end
end
-- Draw down marks
for k, v in pairs(downMarks) do
brightness = util.round(util.linlin(0, DOWN_ANI_LENGTH, 0, DOWN_BRIGHTNESS, v.time_remaining))
for i = 1, grid_w do
if gridLEDs[i][v.y] < brightness then gridLEDs[i][v.y] = brightness end
end
for i = 1, grid_h do
if gridLEDs[v.x][i] < brightness then gridLEDs[v.x][i] = brightness end
end
if v.active and gridLEDs[v.x][v.y] < INACTIVE_BRIGHTNESS then gridLEDs[v.x][v.y] = INACTIVE_BRIGHTNESS end
end
-- Draw remove animations
for _, v in pairs(removeAnimations) do
brightness = util.round(util.linlin(0, REMOVE_ANI_LENGTH, 0, 15, v.time_remaining))
if v.orientation == "row" then
for i = 1, grid_w do
if gridLEDs[i][v.position] < brightness then gridLEDs[i][v.position] = brightness end
end
else
for i = 1, grid_h do
if gridLEDs[v.position][i] < brightness then gridLEDs[v.position][i] = brightness end
end
end
end
-- Draw notes
for _, n in pairs(notes) do
if n.active then brightness = ACTIVE_BRIGHTNESS
else brightness = INACTIVE_BRIGHTNESS end
if n.position <= grid_w then
local ny
for i = 0, n.length - 1 do
ny = n.head + (i * n.direction * -1)
ny = (ny - 1) % params:get("pattern_height") + 1
if ny > 0 and ny <= grid_h then
gridLEDs[n.position][ny] = brightness
end
end
end
end
-- Draw triggers
for _, t in pairs(triggers) do
if t.active then brightness = ACTIVE_BRIGHTNESS
else brightness = INACTIVE_BRIGHTNESS end
if t.position <= grid_h then
local tx
for i = 0, t.length - 1 do
tx = t.head + (i * t.direction * -1)
tx = (tx - 1) % params:get("pattern_width") + 1
if tx > 0 and tx <= grid_w then
gridLEDs[tx][t.position] = brightness
end
end
end
end
for x = 1, grid_w do
for y = 1, grid_h do
gridDevice:led(x, y, gridLEDs[x][y])
end
end
gridDevice:refresh()
end
function redraw()
screen.clear()
--Scale name
screen.move(5, 10)
screen.level(15)
screen.text(MusicUtil.note_num_to_name(rootNote) .. " " .. tonality)
if string.find((MusicUtil.note_num_to_name(rootNote)),"%#") then
screen.move_rel(-5,0)
end
screen.move(99, 10)
screen.level(3)
screen.text('jump: ')
if shiftA then screen.level(15) else screen.level(3) end
screen.text(pivotAmount)
screen.move(100,17)
screen.level(3)
screen.text('octv: ')
screen.text(math.floor(gridScale[1]/12))
screen.move_rel(-29,7)
screen.move(128,60)
if shiftA then screen.level(15) else screen.level(3) end
screen.text_right(options.traffic_length_NAMES[params:get("traffic_length")])
local patternDisplay
if pat.rec == 1 then
patternDisplay = 'rec'
else
if progression.isplaying then patternDisplay = 'pattern'
else patternDisplay = nil end
end
if patternDisplay then
screen.move(128,53)
screen.level(3)
screen.text_right(patternDisplay)
else
screen.move(128,53)
screen.level (0)
end
-- Scale notes
local x, y = 5, 14
local scale_note_names = MusicUtil.note_nums_to_names(gridScale, false)
local COLS = 4
if #scale_note_names <17 then
for i = 1, grid_w do
if (i - 1) % COLS == 0 then x, y = 5, y + 11 end
local is_active = false
for _, n in pairs(notes) do
if n.position == i and n.active then
is_active = true
break
end
end
local underline_length = 10
if scale_note_names[i] == nil then
break
elseif string.len(scale_note_names[i]) > 3 then
underline_length = 18
elseif string.len(scale_note_names[i]) > 2 then
underline_length = 16
end
if is_active then screen.level(15)
else screen.level(3) end
screen.move(x, y)
screen.text(scale_note_names[i])
x = x + 18
end
end
screen.update()
end
function cleanup()
pat:stop()
pat = nil
end