Norns scripting - Link + syncing bar starts (global clock)

I love the Ableton Link support!! Really exciting.

However… while the Norns script I’m working on now syncs to the great Samplr app on my iPad in terms of tempo, it’s pot-luck where in the Samplr bar Norns syncs to.

How do I get my script to sync to bar starts in the other Link clients?

that’s where quantum comes into play — see the phase synchronization section here: https://ableton.github.io/link/

if your apps don’t support custom quantum, you’ll want to play around with that parameter on your norns to see what works best. let us know how it goes and which quantum works well with what apps.

1 Like

Thinking about it, I realise my script has a “step 1”, but no real concept of that being the start of the pattern, so it’s not going to work without my implementing that in some form.

2 Likes

precisely. you can either use clock.get_beats() to check the current time in beats and act accordingly, or sync to whole bars when starting your music phrases - with link quantum size of 4 that will be 4 beats respectively, so clock.sync(4) is guaranteed to resume the coroutine at the beginning of next quantum.

2 Likes

That’s really useful, thank you!

–UPDATE

Should this work (call reset_clock() every bar, and prestep() every 1/8th)?

function pulse()
    while true do
      clock.sync(4)
      reset_clock()
      clock.sync(1/8)
      prestep()
    end
  end

It doesn’t appear to- prestep() runs every bar, and reset_clock doesn’t seem to get called at all.

with the code above:

  • reset_clock will be called every 4th beat (4, 8, 12, etc.)
  • prestep will be called every 4th+1/8th beat (4.125, 8.125, 12.125, etc)

if you want prestep to be called every 1/8th of a beat, just move it to a separate coroutine.

I tried that, like this (in my init() function):

  function reset()
    while true do
      clock.sync(4)
      reset_clock()
    end
  end
  clock.run(reset)
  
  function pulse()
    while true do
      clock.sync(1/8)
      prestep()
    end
  end
  clock.run(pulse)

But it works very badly. The two clocks seem to drift badly out of sync, and I end up getting truncated bars and/or the first step playing more than once.

I think the problem is partly that reset_clock() and prestep() get triggered at almost (but not quite) the same time.

Since reset_clock() resets the step counter, that has to happen before the next step is played by a function called from prestep().

It sounds like that’s not happening reliably, though, and the step counter is sometimes being reset after the step is played (double-triggering first step).

– UPDATE

When clock source is set to “internal”, it seems to work reliably.

The glitchy behaviour happens when source is set to “link”. I initially tried it without another Link device for it it sync to, but now I’m using it with Samplr, and it’s all over the place frankly.

I’ve tried with clock source set to “midi” and “crow” (albeit with no actual clock coming in from those devices), and it’s similarly glitchy, so I don’t think it’s caused by congestion on my WiFi network or any external factor like that.

Incidentally, I also notice there is a “reset” item in the parameters menu, but it doesn’t seem to have a settable value.

– UPDATE TO THE UPDATE

Even when set to internal clock, it glitches every so-often.

so can you show the complete code of your script? what exactly is reset_clock?

The script is quite long, but I think these are the relevant bits:


local ControlSpec = require 'controlspec'
local util = require 'util'

engine.name = "GridKit"

-- Include pattern generator
local patterngen = include('lib/patterngenerator')

-- Include UI
local ui = include('lib/ui')

-- Step position
local step = 0

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

function init()
 -- params stuff

  -- Auto-save params to disk at 'data/<scriptname>/<scriptname>-01.pset' every 10 seconds
  metro_save = metro.init(function(stage) params:write(nil) end, 10)
  metro_save:start()
  
  -- Clocks
  
  function bar()
    while true do
      clock.sync(4)
      reset_counter()
    end
  end
  clock.run(bar)
  
  function pulse()
    while true do
      clock.sync(1/8)
      prestep()
    end
  end
  clock.run(pulse)
  
  -- Render
  ui.init()
  redraw()
end -- end init()

-- Reset clock
function reset_counter()
  step = 0
end

-- Function runs before step
function prestep()
  
  -- debug
  --print(clock.get_beats())
  
  do_step()
end

function do_step()
  
  -- Update step counter
  step = step + 1

  -- Call pattern generator
  triggers = patterngen.get_triggers(step)

  -- Trigger Kick
  if triggers['kick']['trig'] then
    if triggers['kick']['accent'] then
      engine.trig_kick(0.5)
    else
      engine.trig_kick(0.3)
    end
  end
  ui.set_step(step)
  redraw()
  
   -- Reset counter if required
   if step == 32 then
      step = 0
  end
end --end do_step()

To summarise; there’s a step-counter that drives the sequencer/pattern-generator.

I’m trying to reset the step-counter every 4 beats.

Source “link”, no other link device on network

Source “link”, synced to Samplr on iPad

seems to me that there are plenty of other suspects for timing here:
the auto saving,
ui.set_step, redraw, patterngen.get_triggers(),
and the engine itself.

(not saying it isn’t the clock, just that the test case could be simpler and we really would need to see the rest of the stuff that is happening.)

IOW: the lua layer is single threaded. it doesn’t matter how precise the callbacks are if the main thread is bogged down with file writes or drawing or what have you…

1 Like

I’ve stripped it right back for testing porpoises. Try this one:

link_clock_test.zip (4.7 KB)

– UPDATE

This version doesn’t even trigger a sound, and I’m still getting resets on step 16, 17 and 18.

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

local ControlSpec = require 'controlspec'

engine.name = "GridKit"

-- Step position
local step = 1

-- 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,1,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,1,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},
}

--------------------
-- 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.5))
  params:add_control("thresholdsnare","density snare",ControlSpec.new(0.0,1.0,'lin',0,0.25))
  params:add_control("thresholdhat","density hat",ControlSpec.new(0.0,1.0,'lin',0,0.8))
  
  -- Norns new Global Clock (with link support!!)
  
  function bar()
    while true do
      clock.sync(4)
      reset_counter()
    end
  end
  clock.run(bar)
  
  function pulse()
    while true do
      clock.sync(1/4)
      pre_step()
    end
  end
  clock.run(pulse)

end -- end init()

-- Reset clock
function reset_counter()
  print("RESET: count: " .. step)
  screen.clear()
  screen.move(5, 10)
  screen.text("RESET: count: " .. step)
  screen.update()
  step = 1
end

-- Function runs before step
function pre_step()
  
  -- debug
  --print(clock.get_beats())
  
  -- Run step function
  do_step()
  
  -- Update step counter
  if step == 32 then
    step = 0
  else
    step = step + 1
  end
end

function do_step()
	--print("step".. step)
	
-- 	if pattern[1][step] > (1 - params:get('thresholdkick')) then
-- 		engine.trig_kick(0.3)
-- 	end
	
-- 	if pattern[2][step] > (1 - params:get('thresholdsnare')) then
-- 		engine.trig_snare(0.3)
-- 	end
	
-- 	if pattern[3][step] > (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()


2 Likes

ah i see! thank you. sorry for being dense and for the long back/forth.

this just seems like a classic race condition. pulse and bar are backed by separate threads. every 16 pulses, both threads will post an event “at once.” there is no guarantee that events from bar will be handled by the main thread before events from pulse.

if there is a dependency on their order of execution, and everything is supposed to happen in lockstep anyways, then i really don’t see a better way than just using a soft timer

reset_count = 0
reset_period = 16
increment_reset_count = function() 
  reset_count = reset_count + 1
  if reset_count == reset_period then 
    reset_count = 0
    return true
  else
    return false
  end
end

handle_pulse = function()
  if increment_reset_count() then perform_reset() end
  perform_step()
end

function pulse() 
  while true do 
    clock.sync(1/4)
    handle_pulse()
  end
end
clock.run(pulse)

i am surprised by the audible timing issues in the audio samples, since they are not congruent with anything i’ve witnessed - that’s why i had to wonder about other parts of the program. (callbacks from internal clock should not have detectable jitter, but of course they can trigger operations in the script which block the main thread indefinitely.) beyond that, i can’t speak specifically to clock timing as well as @artfwo and others can.

3 Likes

True. I tend to forget when things are asynchronous, and had forgotten the significance of coroutines running on separate threads.

The problem with your solution above, unfortunately, is that (unless I’m missing something obvious) the reset is no longer synced to the bar-start/Link quantum- so we might be clocked with other Link devices, but the bar starts won’t be synced (which was the point of do_reset()).

Maybe I need to setup a 4-beat clock then just use that to kick off the 16th-note clock the first time I get a pulse from it, and hope everything stays in sync from then on.

It would be useful to be able to setup a reset period for a clock, so eg it passes something to the callback function to identify beat 1 of a bar, or even just passes a beat counter which resets to 1 every specified reset period.

The fact Clock has an undocumented “reset” parameter suggests this might be on the cards. Care to comment, @artfwo?

It didn’t just sound like an issue with the counter-resetting. It actually appeared the clock pulses were audibly irregular in some cases.

sorry! of course. this thread is getting long and i am catching up with your original intention.

did you try clock.get_beats()?

hmm. i’m getting predictable results with Ableton Live (with a “1 bar” global quantization) and clock thru Link (with “4” quantum) – using the method @zebra just recommended:

show_me_beats = clock.get_beats() % 4

that gets me a 1 -> 4 count that lines up exactly with Live’s transport with “1 bar” global quantization. when i press play on Live, it waits until it’s at a “1” to start – which is exactly when norns is at its “1”

what are you using to sync to norns through Link? Live or an iOS app? happy to test on my side with specific apps if I have 'em :slight_smile:

No problem! It is getting quite long.

That seems to yield a number of beats since the clock was started at last system boot. I guess I could round it down then % it with 16 to get a counter from 0-15, but would that necessarily mean that “beat 0” was aligned to bar starts for other Link clients?

Ah, you posted while I was writing to @zebra.

That’s encouraging.

I’ve been using Samplr on iOS. I can highly recommend it, if you don’t already have it!

Having said that, maybe this is an issue with Samplr. Or User Error on my part.

fwiw, it looks to me like we are updating the reference beat from the link session.

but for other types of clocks (internal, &c), it’s true that the zero-point reference is the top of main().

(i think i’ve done enough damage here, gonna stick to my own jobs!)

Er… I have a feeling this might be a non-issue, actually.

I’ve just (re)discovered how to start and stop Samplr’s transport. When starting it, it waits for a little while before it actually starts playing (as @dan_derks observed Live doing).

I don’t have access to my Norns right now (it’s running, but my partner is video-conferencing in our shared home-working “office”).

I suspect the pause in Samplr starting is it waiting to sync with the Norns, though.

Really sorry if I’ve wasted everyone’s time.

i’ve been running your script for a while now against link clock with the function do_step rewritten as follows:

function do_step()
  engine.trig_kick(0.1)
end

the rhythm sounds super stable, so the timing issue is likely somewhere else (engine, perhaps, or the provided thresholds result in irregular rhythm?). i also suggest moving all the drawing code to “redraw”, because otherwise your reset_counter function consistently clears and overdraws the menu screen.