20 characters of let me try that!

Hmm… that plays 15 kicks on 16ths, then stops for a bar. It does sync to Samplr bars though, which is great!

hmm, are you sure that the for loop has 15 repetitions? what does your complete script look like right now?

I meant 15, sorry.

The complete script is here (I’m commented out everything that previously triggered sound, so it’s just your clock setup in init() that’s making any sound.

--
-- Link Clock Test
--
-- v0.5.5 @toneburst

local ControlSpec = require 'controlspec'

engine.name = "GridKit"

-- Step position
local step = 0

-- States of the 3 Norns buttons
local button_states = {0,0,0}

local pattern = {
	{1,0,0,0,0,0,0.568627451,0,0,0,0,0,0.8549019608,0,0,0,0.2823529412,0,0.1411764706,0,0.7137254902,0,0,0,0.4274509804,0,0,0,0.2823529412,0,0,0},
	{0.1411764706,0,0.4274509804,0,0,0,0.031372549,0,0.95,0,0,0,0,0,0.2823529412,0,0,0,0.7137254902,0,0,0,0.1411764706,0,0.8549019608,0,0,0,0.568627451,0,0,0},
	{0.6666666667,0,0.4431372549,0,0.9,0,0.2196078431,0,0.6666666667,0,0.5529411765,83,0,0.7764705882,0,0.2196078431,0,0.6666666667,0,0.4431372549,0,0.8862745098,0,0.1098039216,0,0.6666666667,0,0.4431372549,0,0.7764705882,0,0.3333333333}
}

reset_count = 0
reset_period = 16

--------------------
-- Main ------------
--------------------

function init()
  
  -- Restart screen saver timer
  screen.ping()
  -- Default render Style
  screen.level(15)
  screen.aa(0)
  screen.line_width(1)
  screen.font_size(8)
  
  params:add_control("thresholdkick","density kick",ControlSpec.new(0.0,1.0,'lin',0,0.1))
  params:add_control("thresholdsnare","density snare",ControlSpec.new(0.0,1.0,'lin',0,0.0))
  params:add_control("thresholdhat","density hat",ControlSpec.new(0.0,1.0,'lin',0,0.0))
  
  -- https://llllllll.co/t/norns-scripting-link-syncing-bar-starts-global-clock/31734/12?u=toneburst
  local function sequence()
    
    while true do
      clock.sync(4)

      for i=1,15 do
        engine.trig_kick(0.3)
        clock.sync(1/4)
      end
    end
  end
  clock.run(sequence)

end -- end init()

function do_step()
  
  print("count: " .. reset_count)
	--print("step".. step)
-- 	if pattern[1][reset_count + 1] >= (1 - params:get('thresholdkick')) then
-- 		engine.trig_kick(0.3)
-- 	end
	
-- 	if pattern[2][reset_count + 1] >= (1 - params:get('thresholdsnare')) then
-- 		engine.trig_snare(0.3)
-- 	end
	
-- 	if pattern[3][reset_count + 1] >= (1 - params:get('thresholdhat')) then
-- 		engine.trig_hat(0.3)
-- 	end
end -- end do_step()

--------------------
-- Interactions ----
--------------------

function key(id,state)
  
end -- end key()

function enc(id,delta)
  
end --end enc()

--------------------
-- Render ----------
--------------------

function redraw()
 
end -- end redraw()

The 15 kicks are beautifully synced to Samplr, though!

I’ve tried changing the number of iterations and the divisor, but it always seems to leave a silent bar after running through the loop.

Tempo changes track beautifully at either end.

A couple of other little things I noticed:

  • The tempo in the params list doesn’t seem to update when tempo is changed from another Link client (or at least not in realtime)
  • Also, sometimes (but not every time, I think), Norns forces the tempo of Samplr to change when the script starts, rather than syncing to the tempo already established by the other Link client. This may be intended behaviour, but doesn’t seem to be 100% consistent.

i think i could finally reproduce the issue.

there might be a bug in the scheduler – it schedules coroutine resume events too late in the future – sync(4) actually resumes at 8, but can’t say why yet, this didn’t happen before. i’ll have a look at it this week, stay tuned for updates!

1 Like

Ah, cool. Happy to help catch a bug, and good to see, when the bug’s been squashed, this should be relatively simple to achieve!

Is there any workaround to get it to work, with the Clock in its current state?

yes, the workaround would be:

  local function sequence()
    while true do
      for i=1,16 do -- tick counter inside the bar
        engine.trig_kick(0.1)
        clock.sync(1/4)
      end
    end
  end

  clock.run(function()
    clock.sync(4)
    clock.run(sequence)
  end)
2 Likes

That works! Thank you!!

Swing?

Is delaying every other execution of the step function using a metro, like this a sensible way to approach this?

local step = 1
local swingometer = 1

-- Called on 16th notes
function pre_step()

  -- 1 16th-note period
  local sixteenth = clock.get_beat_sec() / 4
  
  swingometer = (swingometer * -1)
  
  local swing_delay = (clock.get_beat_sec() / 4) * params:get('swing')
  
  if swingometer < 0 then
     local metro_swing = metro.init(function(stage) do_step() end, swing_delay, 1)
     metro_swing:start()
  else
    do_step()
  end
  
end

This works for a few steps, but then fails with error

lua: /home/we/norns/lua/core/clock.lua:82: /home/we/dust/code/beat_explorer/link_clock_test.lua:75: attempt to index a nil value (local ‘metro_swing’)

Just to let you know, I retro-fitted your most recent clock setup to my Grids port script, and all is now working great, so thank you again @artfwo!

1 Like

while it’s possible, i wouldn’t recommend mixing metro callbacks and coroutines for the sake of code clarity. you can use clock.sleep(x) inside the coroutine to resume its execution in x seconds.

PR created and a fix will be available in the next update. Thanks for reporting the issue!

How might that look?

Wouldn’t the clock re-sync itself when it resumes, defeating the object of being able to shift every other pulse an arbitrary amount?

Happy to help!

basically depends on how much you sleep, i.e.

function seq()
  local beat_duration = clock.get_beat_sec()

  clock.sync(4) -- will resume at 4
  clock.sleep(beat_duration / 4) -- will resume at 4.25
  clock.sync(4)  -- will resume at 8
  clock.sleep(beat_duration * 5) -- will resume at 13
  clock.sync(4)  -- will resume at 16
end

but if you want to resume at a particular fraction of a beat, better use clock.sync, as it will take the reference beat into account.

2 Likes

I’ve been playing around with implementing a 16th note swing based on the conversation above, and have it working, but don’t entirely understand why.

For example, I would expect if I am running clock.sync(1/4) at 60bpm, a 16th note happens every .25 seconds. So, to get a swing I could delay every other note by up to ~ .24 seconds, right? It’s very likely I’m being dumb at basic math (its been known to happen…)

Instead, swing stops working at about 0.125 (a 32nd note), and the sync rate sounds as if it slows down to a different beat division. If I cap the value it’s working great however. Maybe I’m thinking about this the wrong way?

Here’s my script, if anyone’s interested (with the offending line commented out):

engine.name = 'PolyPerc'

local swing = 0

function pulse(freq)
  while true do
    clock.sync(1/4)
    local swing_amt = params:get('swing')
    if swing == 1 and swing_amt > 0 then
      -- local delay = (clock.get_beat_sec()/4) * (swing_amt/100) -- doesnt work when swing above 49%
      local delay = (clock.get_beat_sec()/4) * (swing_amt*.49/100) -- value scaled to work up to 100%
      clock.sleep(delay)
      engine.hz(freq)
    else
      engine.hz(freq)
    end
    swing = (swing + 1) % 2
  end
end

function init()
  params:add_control('swing', 'swing', controlspec.new(0, 100, 'lin', 0, 0, '%'))
  clock_id = clock.run(pulse, 70)
end
1 Like

the code looks correct, but clock.sync might have a bug causing it to skip sync points, when you’re calling it shortly before the next sync point (<50% of the sync division actually).

a fix is already in norns master, so you can try that, if you’re comfortable building code, or wait till the next update and check if that solves the problem.

1 Like

Is that the same bug I found?

20 characters of yes :slight_smile:

1 Like

Is this fix applied in the latest update? I’m converting a script I’m working on to use the new clock system, and when I change the source from internal to crow it sounds rather imprecise, like a jittering clock or randomly missed triggers.

yes, the fix is released since norns 200604. could you share the (ideally minimal possible) code that exposes the issue?

The entire script is pretty minimal, bear in mind this is my very first attempt, trying to learn norns, lua, and a new sc engine at the same time, so I would not be surprised if I made an error somewhere.

--  arpcore
--  v0.01 by robotfunk
--  
--  /////////////////////
--  / / / / / / / / / / / / /
--  /  /  /  /  /  /  /  /  /  /  /
--  /   /   /   /   /   /   /   /   /   /   /
--  /    /    /    /    /    /    /    /    /    /    /
engine.name = 'KarplusRings'
musicutil = require 'musicutil'
er = require 'er'
mode = 1
toniclist = musicutil.NOTE_NAMES
tonicnum = 1
mode_name = musicutil.SCALES[mode].name
m = midi.connect()
m:start()

function init()
  generate_notes()
  noteIndex = 1
  length = 3
  stride = 1
  offset = 0
  pulses = 1
  End = 16
  generate_gates()
  --general params
  --params:add{type = "option", id = "use_karplus_rings", name = "Use Karplus Rings", options = {'yes','no'}, default = 1}
  params:add{type = "option", id = "send_midi", name = "Send MIDI Out", options = {'yes','no'}, default = 1}
  params:add{type = "option", id = "send_crow", name = "Send Crow Out", options = {'yes','no'}, default = 2}
  params:add{type = "option", id = "crow_outs", name = "Crow Outputs", options = {'1/2','3/4'}, default = 1}
--  params:add{type = "option", id = "send_osc", name = "Send OSC Out", options = {'yes','no'}, default = 2}
  params:add_separator()
--KarplusRings params
  --cs.AMP = cs.new(0,1,'lin',0,0.5,'')
  params:add_control("amp","amp", controlspec.new(0,1,"lin",0,0.5,""))
  params:set_action("amp", function(x) engine.amp(x) end)
  --cs.BPF_FREQ = cs.new(100,10000,'lin',0,0.5,'')
  params:add_control("bpf_freq","bpf_freq", controlspec.new(100,10000,"lin",0,0.5,""))
  params:set_action("bpf_freq", function(x) engine.bpf_freq(x) end)
  --cs.BPF_RES = cs.new(0,4,'lin',0,0.5,'')
  params:add_control("bpf_res","bpf_res", controlspec.new(1,2,"lin",0,0.5,""))
  params:set_action("bpf_res", function(x) engine.bpf_res(x) end)
  --cs.COEF = cs.new(0,1,'lin',0,0.11,'')
  params:add_control("coef","coef", controlspec.new(0,0.75,"lin",0,0.11,""))
  params:set_action("coef", function(x) engine.coef(x) end)
  -- cs.DECAY = cs.new(0.1,15,'lin',0,3.6,'s')
  params:add_control("decay","decay", controlspec.new(0,15,"lin",0,3.6,"s"))
  params:set_action("decay", function(x) engine.decay(x) end)
  --cs.LPF_FREQ = cs.new(100,10000,'lin',0,4500,'')
  params:add_control("lpf_freq","lpf_freq", controlspec.new(100,10000,"lin",0,4500,""))
  params:set_action("lpf_freq", function(x) engine.lpf_freq(x) end)
  --cs.LPF_GAIN = cs.new(0,3.2,'lin',0,0.5,'')
  params:add_control("lpf_gain","lpf_gain", controlspec.new(0,3.2,"lin",0,0.5,""))
  params:set_action("lpf_gain", function(x) engine.lpf_gain(x) end)
--seq params
--length
  params:add_control("length","length", controlspec.new(1,16,"lin",1,3,""))
  params:set_action("length", function(x) length=x end)--numNotes
--stride
  params:add_control("stride","stride", controlspec.new(-16,16,"lin",1,1,""))
  params:set_action("stride", function(x) stride=x end)
--pulses
  params:add_control("pulses","pulses", controlspec.new(1,16,"lin",1,1,""))
  params:set_action("pulses", function(x) pulses=x end)
  params:bang()
  clk=clock.run(pulse)
end
  
function clock.transport.start()
  clock.cancel(clk)
  clk = clock.run(pulse)
end

function clock.transport.stop()
  clock.cancel(id)
end
  
function pulse()
  while true do
    clock.sync(1/4)
    step()
  end
end

function step()
  if (gates[noteIndex]) then
    engine.hz((440 / 32) * (2 ^ ((notes[noteIndex] - 9) / 12)))
    if (params:get("send_midi")==1) then
      chan=1
      note=notes[noteIndex]
      m:note_on(note, 100, 1)
      noteoff_delayed = metro.init()
      noteoff_delayed.time = .05
      noteoff_delayed.count = 1
      noteoff_delayed.event = 
        function(chan, note) 
          m:note_off(note, 0, 1)
          metro.free(noteoff_delayed.id)
        end--function
      noteoff_delayed:start()
    end
  end
  noteIndex = noteIndex + stride
  while (noteIndex < 1) do
    noteIndex = noteIndex + length
  end
  while (noteIndex > length) do
    noteIndex = noteIndex - length
  end
  redraw()
end
  
function key(n,z)
  if ((n==2) and (z==1)) then 
    mode = mode + 1 
    if (mode>#musicutil.SCALES) then mode = 1 end
    generate_notes()
  end
  if ((n==3) and (z==1)) then shuffle(notes) end
end

function enc(n,d)
  if (n==1) then 
    params:delta("stride", d) 
  end
  if (n==2) then 
    params:delta("length", d) 
    generate_gates()
  end
  if (n==3) then 
    params:delta("pulses", d) 
    generate_gates()
  end
end

function get_value_by_shift(t, index, shift)
  index = (index + shift - 1)%#t + 1
  return t[index]
end

function redraw()
  End = (pulses + length) -1
  if (End>16) then End=End-16 end
  screen.clear()
  screen.level(15)
  screen.line_width(1)
  
  for i=1,16 do
    screen.rect( (i-1)*8+1 , 19, 7, 8)
    if (gates[i]) then screen.fill() else screen.stroke() end
  end
  
  screen.level(6)
  for i=1,16 do
    screen.rect( (i-1)*8+1 , 19, 7, 8)
    if (i==(noteIndex)) then screen.fill() else screen.stroke() end
  end
  --screen.stroke()
  --screen.move((pulses-1)*8+3, 25)
  --screen.text(">")
  --screen.move((End-1)*8+3, 25)
  --screen.text("<")
  screen.move(0,40)
  screen.text(mode .. " " .. musicutil.SCALES[mode].name)
  screen.move(0,8)
  screen.text("stride: "..stride)
  screen.move(0,60)  
  screen.text("length: "..length)
  screen.move(92,60)
  screen.text("pulses: "..pulses)
  screen.update()
end

function cleanup()
  -- deinitialization
end

function generate_notes()
  notes = musicutil.generate_scale_of_length(36+(tonicnum-1),musicutil.SCALES[mode].name,16)
end
  
function generate_gates()
  gates = er.gen(pulses, length)
end

function shuffle(tbl)
  for i = #tbl, 2, -1 do
    local j = math.random(i)
    tbl[i], tbl[j] = tbl[j], tbl[i]
  end
end

--copied from foulplay, not (yet?) used
local function rotate_pattern(t, rot, n, r)
  -- rotate_pattern comes to us via okyeron and stackexchange
  n, r = n or #t, {}
  rot = rot % n
  for i = 1, rot do
    r[i] = t[n - rot + i]
  end
  for i = rot + 1, n do
    r[i] = t[i - rot]
  end
  return r
end

thanks, so clock-wise the code looks mostly fine, except these lines:

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

i think you could call clock.cancel(clk) here instead.

crow clock takes signal from input 1. does you script sound of of sync with the signal on input 1?