Skip to content

Instantly share code, notes, and snippets.

@williamthazard
Last active June 12, 2022 01:53
Show Gist options
  • Save williamthazard/fab14a897e2501b930ec2c303bec9b44 to your computer and use it in GitHub Desktop.
Save williamthazard/fab14a897e2501b930ec2c303bec9b44 to your computer and use it in GitHub Desktop.
--- krahenLied.
input[1].mode('clock')
ii.jf.mode(1)
tab = function(t,e)
for index, value in ipairs(t) do
if value == e then return index end
end
return nil
end
Lattice, Pattern = {}, {}
function Lattice:new(args)
local l = setmetatable({}, { __index = Lattice })
local args = args == nil and {} or args
l.auto = args.auto == nil and true or args.auto
l.meter = args.meter == nil and 4 or args.meter
l.ppqn = args.ppqn == nil and 96 or args.ppqn
l.enabled = false
l.transport = 0
l.superclock_id = nil
l.pattern_id_counter = 100
l.patterns = {}
return l
end
function Lattice:start()
self.enabled = true
if self.auto and self.superclock_id == nil then
self.superclock_id = clock.run(self.auto_pulse, self)
end
end
function Lattice:reset()
self:stop()
if self.superclock_id ~= nil then
clock.cancel(self.superclock_id)
self.superclock_id = nil
end
for i, pattern in pairs(self.patterns) do
pattern.phase = pattern.division * self.ppqn * self.meter
end
self.transport = 0
params:set("clock_reset",1)
end
function Lattice:hard_restart()
self:reset()
self:start()
end
function Lattice:stop()
self.enabled = false
end
function Lattice:toggle()
self.enabled = not self.enabled
end
function Lattice:destroy()
self:stop()
if self.superclock_id ~= nil then
clock.cancel(self.superclock_id)
end
self.patterns = {}
end
function Lattice:set_meter(meter)
self.meter = meter
end
function Lattice.auto_pulse(s)
while true do
s:pulse()
clock.sync(1/s.ppqn)
end
end
function Lattice:pulse()
if self.enabled then
local ppm = self.ppqn * self.meter
for id, pattern in pairs(self.patterns) do
if pattern.enabled then
pattern.phase = pattern.phase + 1
if pattern.phase > (pattern.division * ppm) then
pattern.phase = pattern.phase - (pattern.division * ppm)
pattern.action(self.transport)
end
elseif pattern.flag then
self.patterns[pattern.id] = nil
end
end
self.transport = self.transport + 1
end
end
function Lattice:new_pattern(args)
self.pattern_id_counter = self.pattern_id_counter + 1
local args = args == nil and {} or args
args.id = self.pattern_id_counter
args.action = args.action == nil and function(t) return end or args.action
args.division = args.division == nil and 1/4 or args.division
args.enabled = args.enabled == nil and true or args.enabled
args.phase = args.division * self.ppqn * self.meter
local pattern = Pattern:new(args)
self.patterns[self.pattern_id_counter] = pattern
return pattern
end
function Pattern:new(args)
local p = setmetatable({}, { __index = Pattern })
p.id = args.id
p.division = args.division
p.action = args.action
p.enabled = args.enabled
p.phase = args.phase
p.flag = false
return p
end
function Pattern:start()
self.enabled = true
end
function Pattern:stop()
self.enabled = false
end
function Pattern:toggle()
self.enabled = not self.enabled
end
function Pattern:destroy()
self.enabled = false
self.flag = true
end
function Pattern:set_division(n)
self.division = n
end
function Pattern:set_action(fn)
self.action = fn
end
text = "aaaaaaaaaaaaaaaaaa"
function remap(ascii)
local offset
if ascii <= 32 then offset = 0
elseif ascii > 32 and ascii <= 64 then offset = -32
elseif ascii > 64 and ascii <= 96 then offset = -64
elseif ascii > 96 and ascii <= 128 then offset = -96
elseif ascii > 128 and ascii <= 160 then offset = -128
elseif ascii > 160 and ascii <= 192 then offset = -160
elseif ascii > 192 and ascii <= 224 then offset = -192
elseif ascii > 224 and ascii <= 255 then offset = -224
end
return ascii + offset
end
function processString(s)
local tempScalar = {}
for i = 1, #s do
table.insert(tempScalar,remap(s:byte(i)))
end
return tempScalar
end
function jfmap(ascii)
local map
if ascii <= 51 then map = 1
elseif ascii > 51 and ascii <= 102 then map = 2
elseif ascii > 102 and ascii <= 153 then map = 3
elseif ascii > 153 and ascii <= 204 then map = 4
elseif ascii > 204 and ascii <= 255 then map = 5
end
return map
end
function jfscaling (j)
local tempScalar = {}
for i = 1, #j do
table.insert(tempScalar,jfmap(j:byte(i)))
end
return tempScalar
end
s = sequins(processString(text))
j = sequins(jfscaling(text))
function init()
liedclock = Lattice:new{
auto = true,
meter = 4,
ppqn = 4
}
notes_lattice = liedclock:new_pattern{
action = function(t) notes_event() end,
division = 1,
enabled = true
}
other_lattice = liedclock:new_pattern{
action = function(t) other_event() end,
division = 1,
enabled = true
}
jfa_lattice = liedclock:new_pattern{
action = function(t) jfa_event() end,
division = 1/4,
enabled = true
}
jfb_lattice = liedclock:new_pattern{
action = function(t) jfb_event() end,
division = 1/4,
enabled = true
}
jfc_lattice = liedclock:new_pattern{
action = function(t) jfc_event() end,
division = 1/4,
enabled = true
}
jfd_lattice = liedclock:new_pattern{
action = function(t) jfd_event() end,
division = 1/4,
enabled = true
}
liedclock:start()
end
function notes_event()
s:settable(processString(text))
notes_pitch = s
notes_time = s:step(2)
local slew_step = s:step(3)
local slew = slew_step()/300
local notes_slope = { to(5,dyn{attack=1}/20), to(0,dyn{release=1}/20) }
notes_lattice:set_division(notes_time()/32)
local v = notes_pitch()
local freq = v/12
output[1].volts = freq
output[2].action = notes_slope
output[2].dyn.attack = s:step(4)()
output[2].dyn.release = s:step(5)()
output[1].slew = slew
output[2]()
end
function other_event()
s:settable(processString(text))
other_pitch = s:step(6)
other_time = s:step(7)
local slew_step = s:step(8)
local slew = slew_step()/300
local other_slope = { to(5,dyn{attack=1}/20), to(0,dyn{release=1}/20) }
other_lattice:set_division(other_time()/32)
local v = other_pitch()
local freq = v/12
output[3].volts = freq
output[4].action = other_slope
output[4].dyn.attack = s:step(9)()
output[4].dyn.release = s:step(10)()
output[3].slew = slew
output[4]()
end
function jfa_event()
s:settable(processString(text))
j:settable(jfscaling(text))
jfa_pitch = s:step(11)
jfa_time = s:step(12)
jfa_lattice:set_division(jfa_time()/32)
local v = jfa_pitch()
local freq = v/12
local level = j:step(13)()
ii.jf.play_note(freq, level)
end
function jfb_event()
s:settable(processString(text))
j:settable(jfscaling(text))
jfb_pitch = s:step(14)
jfb_time = s:step(15)
jfb_lattice:set_division(jfb_time()/32)
local v = jfb_pitch()
local freq = v/12
local level = j:step(16)()
ii.jf.play_note(freq, level)
end
function jfc_event()
s:settable(processString(text))
j:settable(jfscaling(text))
jfc_pitch = s:step(17)
jfc_time = s:step(18)
jfc_lattice:set_division(jfc_time()/32)
local v = jfc_pitch()
local freq = v/12
local level = j:step(19)()
ii.jf.play_note(freq, level)
end
function jfd_event()
s:settable(processString(text))
j:settable(jfscaling(text))
jfd_pitch = s:step(20)
jfd_time = s:step(21)
jfd_lattice:set_division(jfd_time()/32)
local v = jfd_pitch()
local freq = v/12
local level = j:step(22)()
ii.jf.play_note(freq, level)
end
@trentgill
Copy link

trentgill commented Jun 12, 2022

Apologies for delay. Here's some thoughts & review. From top to bottom:

lines 2&3: should probably be in the init function.

line 4: tab function is unused and the name is confusing. i guess it should be if_contains or something?

I've never used Lattice on crow, so I can't really say whether or not your solution is a good one. Seems to me you could probably work with just the clock library, rather than adding the whole Lattice framework on top.

line 127: remap function seems like it's equivalent to just return n % 32? you're just wrapping everything into the (0..32) range i think.

line 147: jfmap is just return math.floor(n / 51)+1

lines 140+157: processString & jfscaling can be the same function and just pass in jfmap or remap as a function argument to be applied.

i have some misgivings about the way the jf*_event functions & lattice patterns are copy+paste-y, but it's unfortunately more than trivial to refactor this.


that's my general code review for making it a little easier to work on the script.

one thing that i'm very confused about is that you're calling the settable method on the s sequins every time there's an event. processString is a pure function, and text is only changed manually it seems. i'd just make a simple function text which takes a single string argument & calls settable on both s and j. this will massively reduce the CPU usage as settable and your string conversion functions are both expensive (and churn a bunch of RAM due to the gradual table buildup).


ok! one big thing / misunderstanding i'm seeing. at (for eg) line 274ish. you're saving the result of s:step(..) into a variable, then doing it again with a different step value.

firstly, s:step() returns the sequins table itself. it seems from your code that you're assuming this.

secondly though, the sequins object is a mutable data-structure, so your variables jfd_pitch and jfd_time are in fact equal to s itself. what you probably want to do is set the step value and pull a value from the sequins into the named param. something like:

function jfd_event()
    -- s:settable(processString(text)) -- do this only when text changes
    -- j:settable(jfscaling(text)) -- "
-- both jfd_pitch & jfd_time could be `local` too
    jfd_pitch = s:step(20)() -- call the sequins to realize a value
    jfd_time = s:step(21)() -- "
    jfd_lattice:set_division(jfd_time/32) -- jfd_time is a number now
    -- local v = jfd_pitch() -- jfd_pitch is a number, no need to alias it
    local freq = jfd_pitch/12
    local level = j:step(22)() -- you're already doing it this way here!
    ii.jf.play_note(freq, level)
end

ok, finally this leads me to the issue where w/ is crashing the system. there's nothing in your code that should really be causing issues, but just to double-check it is w/ causing the problem (and not general system stability), try changing jfd_event's call to ii.jf.play_note with ii.wsyn.play_note and leave everything else the same. this should work directly, and if crow+jf are stable, it's very likely that crow+w/ should be as well.

if you run into issues by just changing that one line, i'd:

  1. clear the userscript on crow with ^^c
  2. restart the case
  3. send some ii.wsyn.play_note(0,3) messages (or other similar values) directly from druid

this should prove that the system is working, or help reveal instabilities in the system that are unrelated to the script you're running. when testing for stability i often use the up-arrow then enter in rapid succession to generate a bunch of notes (ie execute the same line of code again and again). often an unstable system will work fine with isolated messages, but repeatedly bashing messages into the ii system will reveal faults.

you might need an i2c "bus board" to provide some extra pullup current. see the thread on lines for more info on that.

my primary concern (and the reason i went to the trouble of the code review above) is that the script is putting crow in a state where it's operating near the edge of its capacity & thus revealing instability in the Lua VM. of course we have been trying to improve this kind of reliability, but the system is not perfect. i think the above changes (especially not re-running the text converter in every event) will make a massive difference.


finally since you asked & the docs are lacking, the delay technique in init is like so (however, you are correct that it's not relevant here as you only have the single ii message occuring at startup, so treat this just as an fyi):

-- original version
function init()
  ii.event1()
  ii.event2()
  -- etc
  ii.event9()
  -- etc2
end

-- broken into a delayed form
function init()
  delay( function()
    ii.event1()
    ii.event2()
    -- etc
  end, 0.001) -- delay execution of this anonymous function by 1ms
  delay( function()
    ii.event9()
    -- etc2
  end, 0.002) -- delay execution of this anonymous function by 2ms
end

so delay is just a function that will call another function (it's first argument) at some time in the future. i'm just using an anonymous function inline here, so you don't need to separate the code from the caller. of course you can name the functions if it makes more sense though!

-- a structured init
function init_jf()
  -- up to 8 messages configuring jf
end

function init_wsyn()
  -- up to 8 messages configuring wsyn
end

function init()
  delay(init_jf, 0.001)
  delay(init_wsyn, 0.002)
end

nothing here is a hard & fast solution, but this should be plenty of territory to explore and improve your script!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment