I just did that in my cut-down clock-testing script. The drawing is called from redraw() in the original script.

I’ll try with the reset setup @zebra described, and see how stable it is then.

Hopefully all will then be well, as I don’t think my script is going anything super expensive (though I’m sure what it is doing could be done in a more efficient manner).

Just to be clear @artfwo, if you start a clock coroutine from the init() function of a script, clock source is set to “link”, and there is another link device already on the network, will the clock coroutine wait for the “1” from the other device(s) before it actually starts triggering it’s callback function?

it doesn’t matter when you start the coroutine. clock.sync(n) inside the coroutine will wait until next Nth beat in the future.

for example:

clock.sync(1) will wait and resume the coroutine at next 1st beat: 1, 2, 3, 4, 5, …, 55. if called between beats 1 and 2, it will wait more or less precisely until beat 2 is reached on the clock.
clock.sync(2) will wait and resume the coroutine at next 2nd beat - 2, 4, 6, etc. (but never 3, 5, 7!)
clock.sync(1.5) will resume the coroutine at beats 1.5, 3.0, 4.5, etc.
clock.sync(0.5) will resume the coroutine at beats 0.5, 1.0, 1.5, etc.

it’s called sync because it kinda syncs the coroutine to the clock. hope this helps!

2 Likes

Sorry… I can understand both the meaning of “sync” and your frustration at my probably stupid questions.

However… I’m still not sure the best way to accomplish what I’m fairly sure is a pretty basic task, ie having a pulse synced to Link 1/4 beats, and having a Norns sequencer always play ‘step 1’ exactly when other Link client start a bar.

There must be a simple way to accomplish this basic task…

It’s not impossible that the problem lies with the iOS app I’m using, of course. Samplr is generally great, but it’s Link support may not work that well.

so again this depends on link quantum size, let’s assume it’s 4. then you can rely of the fact that clock.sync(4) will resume a coroutine at every 4th beat[1] or every bar of the other link apps.

more specifically, the following code might help you get started:

local function sequence()
  while true do
    clock.sync(4)

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

it will play 15 kicks every 4 beats (1 bar), always beginning at next 4th beat, no matter when the coroutine is started.


[1] link automatically synchronises phases of clients using their quantum setting.

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?