Using midi cc to create a trigger sequencer ("sleep" function help)

EDIT: thanks @xmacex simple solution is below. I’d like to add that this whole approach should be easy to implement with any midi to cv converter as long as you can dedicate outputs to cc channels.

Hi all,

I’m scripting a trigger sequencer so I can use norns on some of my percussion modules and the bare bones are done.

The way I’m creating the triggers is by setting my cv outputs on my midi > cv modules to dedicated cc channels and scripting the cc amount to be 127 wait a fraction of a second and then be 0

trigger code

function trigon()
poly2:cc(1,127,1)
clock.run(trigoff)
end
end

function trigoff()
clock.sleep(0.05)
poly2:cc(1,0,1)
end

The code above is the simplest way I could send the cc high, wait, and then low again. To incorporate it into my sequence I simply insert a value into the function via function trigon(t) and if t = 1 (a step in the sequence is on) the function runs. This is all works smooth as butter when there is only one sequence track running but, the moment I change this function to include the cc channel it should be sending to the whole script crashes the moment I hit a trigger.

faulty trigger code

function trigon(ch,t)
if t == 1 then
poly2:cc(ch,127,1)
clock.run(trigoff(ch))
end
end

function trigoff(ch)
clock.sleep(0.05)
poly2:cc(ch,0,1)
end

The specific line I suspect to be the problem here is “clock.run(trigoff(ch))”.

Any advice as to why I’m encountering this problem, or if there is a different way I could handle the wait to turn my triggers off?

maiden

lua: /home/we/norns/lua/core/clock.lua:82: /home/we/norns/lua/core/clock.lua:19: bad argument #1 to ‘create’ (function expected, got nil)
stack traceback:
[C]: in function ‘error’
/home/we/norns/lua/core/clock.lua:82: in function ‘core/clock.resume’

Full code. only channel 1 working

include(‘midigrid/config/launchpadmini_config’)
include(‘midigrid/lib/midigrid’)
local grid = include(‘midigrid/lib/mg_128’)

g = grid.connect()
function go()
norns.script.load(norns.state.script)
end

– declare variables

steps = {}
–going to change stepcount into nested table so each track can have different sequence lengths
stepcount = 8
position = 1
channel = {1,2,3,4}
x_pos = 1
y_pos = 1
pressed = 1
pad = {
{1,5,9 ,13},
{2,6,10,14},
{3,7,11,15},
{4,8,12,16}
}

– for new clock system
function pulse()
while true do
clock.sync(1/4)
count()
end
end

function clock.transport.stop()
clock.cancel(clock_id)
end

function clock.transport.start()
clock_id = clock.run(pulse)
end

function init()
clock.transport.start()
poly2 = midi.connect(1)
g:all(0)

– default all trigger sequences to 0
for track=1,stepcount do
steps[track] = {}
for step=1,16 do
steps[track][step] = 0
end
end

grid_redraw()
position = 1
end
g.key = function(x,y,z)

– ignore as trigon has changed since implementing the sequencer to be fixed
if z == 1 and x <= 4 and y <= 4 then
x_pos = x
y_pos = y
pressed = pad[x_pos][y_pos]
print (pressed)
trigon()

– switches a steps state on or off // ofsets to use bottom four rows on grids
elseif z == 1 and y >= 5 then
if steps[channel[y-4]][x] == 0 or nil then
steps[channel[y-4]][x] = 1
else
steps[channel[y-4]][x] = steps[channel[y-4]][x] - steps[channel[y-4]][x]
end
print(“ch: " …y-4 …” step “…x…”: "… steps[channel[y-4]][x])
grid_redraw()
end

end

– uses midi cc to create a trigger // currently only triggering on channel 1 for testing
function trigon(t)
if t == 1 then
poly2:cc(1,127,1)
clock.run(trigoff)
end
end

function trigoff()
clock.sleep(0.05)
poly2:cc(1,0,1)
end

function draw_active()
for track = 1,4 do
for step = 1, stepcount do
g:led(step,track+4,steps[track][step])
g:refresh()
end
end
end

function draw_step()
for track = 1,4 do
g:led(position,track+4,15)
end
end

function grid_redraw()
g:all(0)
draw_active()
draw_step()
g:refresh()
end

function count()
g:refresh()

– itterates through steps and sends a trigger if steps =1 // currently only triggering on channel 1 for testing
position = (position % stepcount) + 1
trigon(steps[1][position])
grid_redraw()
end

function cleanup ()
clock.cancel(clock_id)
end

full code faulty

include(‘midigrid/config/launchpadmini_config’)
include(‘midigrid/lib/midigrid’)
local grid = include(‘midigrid/lib/mg_128’)

g = grid.connect()
function go()
norns.script.load(norns.state.script)
end

– declare variables

steps = {}
–going to change stepcount into nested table so each track can have different sequence lengths
stepcount = 8
position = 1
channel = {1,2,3,4}
x_pos = 1
y_pos = 1
pressed = 1
pad = {
{1,5,9 ,13},
{2,6,10,14},
{3,7,11,15},
{4,8,12,16}
}

– for new clock system
function pulse()
while true do
clock.sync(1/4)
count()
end
end

function clock.transport.stop()
clock.cancel(clock_id)
end

function clock.transport.start()
clock_id = clock.run(pulse)
end

function init()
clock.transport.start()
poly2 = midi.connect(1)
g:all(0)

– default all trigger sequences to 0
for track=1,stepcount do
steps[track] = {}
for step=1,16 do
steps[track][step] = 0
end
end

grid_redraw()
position = 1
end
g.key = function(x,y,z)

– ignore as trigon has changed since implementing the sequencer to be fixed
if z == 1 and x <= 4 and y <= 4 then
x_pos = x
y_pos = y
pressed = pad[x_pos][y_pos]
print (pressed)
trigon()

– switches a steps state on or off // ofsets to use bottom four rows on grids
elseif z == 1 and y >= 5 then
if steps[channel[y-4]][x] == 0 or nil then
steps[channel[y-4]][x] = 1
else
steps[channel[y-4]][x] = steps[channel[y-4]][x] - steps[channel[y-4]][x]
end
print(“ch: " …y-4 …” step “…x…”: "… steps[channel[y-4]][x])
grid_redraw()
end

end

– uses midi cc to create a trigger // currently only triggering on channel 1 for testing
function trigon(ch,t)
if t == 1 then
poly2:cc(ch,127,1)
clock.run(trigoff(ch))
end
end

function trigoff(ch)
clock.sleep(0.05)
poly2:cc(ch,0,1)
end

function draw_active()
for track = 1,4 do
for step = 1, stepcount do
g:led(step,track+4,steps[track][step])
g:refresh()
end
end
end

function draw_step()
for track = 1,4 do
g:led(position,track+4,15)
end
end

function grid_redraw()
g:all(0)
draw_active()
draw_step()
g:refresh()
end

function count()
g:refresh()

– itterates through steps and sends a trigger if steps =1 // currently only triggering on channel 1 for testing
position = (position % stepcount) + 1
for ch = 1,4 do
trigon(ch,steps[1][position])
end
grid_redraw()
end

function cleanup ()
clock.cancel(clock_id)
end

1 Like

From what I understand just looking at the code and glancing at clock docs and clock.run reference.

the why bit

If you think of execution order of the latter, parametrized code which gives the error and what your function trigoff returns (nothing, aka nil), you’ll see that what happens are the following steps. Let’s presume value of ch to be, say 1.

1. clock.run(trigoff(ch)) → clock.run(trigoff(1)) # value substitution
2. clock.run(trigoff(1)) → clock.run(nil) # function call returns no return value
3. clock.run(nil) → ☠️ # error because there is no parameter

Basically the issue is that this is not the execution order you want.

the alternative solutions bit

Three alternatives (all good fairytales have three alternatives, no?) are firstly lambda function aka anonymous function to postpone the trigoff execution. Secondly, storing the value of ch in a variable of outer scope (even global variable, preferably some nice data structure like a table) rather than passing it as argument to trigoff. This code you already have in trigger code and is working, but has hardcoded channel.

Thirdly the clock.run has this considered already :slight_smile: I would consider this the preferred solution. Looking at the clock docs, the simple example is

function init()
  print("starting now")
  clock.run(later)
  print("done with init")
end

function later()
  clock.sleep(2)
  print("now awake")
end

in which you’ll see that clock.run parameter is only the name of the function later, not a call to it. However, further down the page in the arguments section there’s a good solution for you because clock.run can pass the parameters to the named function. So in your case is

clock.run(trigoff, ch)

which clock.run turns to trigoff(ch) later, when it’s the right time.

I hope my analysis is correct and helps you forward. And thanks for this opportunity to procrastinate, i should be working lol. Actually writing some R+tidydata, in functional style no less.

PS. autobiographical remarks

It took me literally 400 years to understand what these function call things were when they were postponed, web UI JavaScript is absolutely full of it, evident in the mad indentation cascades you sometimes see when JS flows off the right side of the screen.

The idea is really simple and it’s all in basic programming tutorials and courses, but i somehow failed to take it seriously and learn it until I did. Seen through firstly functions as input transformations to outputs, and secondly memorizing the execution order which is an extension of the familiar from school math and propositional logic stuff it all makes sense and is super foundational, and also binds together various programming paradigms. Reading code through execution order rather than lines of source code is the key to success, at least it was in my case. Since my revelation Lisp, functional data science stuff with R with *apply and tidydata, functional Python, the Haskell-based TidalCycles live-coding algorave thing and yes also those pesky JavaScript UIs make sense… most of the time.

Thanks @xmacex that was extremely helpful!

I tried creating a quick global variable but in the end, found the following to be the easiest.

 function trigon(ch,t)
    if t == 1 then 
     poly2:cc(ch,127,1)
     clock.run(trigoff)
    end 
  end
  
  function trigoff()
    clock.sleep(0.05)
    for i = 1,#channel do
      poly2:cc(i,0,1)
    end
  end

As I’m mostly interested in the rising edge of the trigger anyway I’m just cycling through all the channels to make sure the triggers are off before the next one hits.

I’m sure in the future I might add more functions for gates but that should be as easy as " while x = 1 cc = 127 and while x = 0 cc = 0 "

Thanks again for the great advice and for using your procrastination to get my little project working.

1 Like