norns: clock

hey sorry, i saw this and then we emailed and i totally spaced on replying here :slight_smile:

i know this one is frustrating. essentially, investigation is still underway – there was a change released in december 2020 which didn’t end up solving all cases. a solid set of repro cases have been logged and we’ll definitely keep folks updated as work develops! for now, clocking from norns (if possible) is the best approach. this has been logged, tho, and won’t get lost (it’s part of feasibility scoping for norns 3.0) – thanks for the patience + bump :rainbow:

(script authors can also bypass the norns system clock and listen directly for incoming crow pulses or midi clock messages when using those clocking methods – this is a workaround until the core library has been addressed)

as far as transport goes, methods are now outlined in the clock studies for script authors: clocks - docs. this is pretty much the method i used to add a global transport to cheat codes. if you have a favorite script that you want to talk to the transport, it’d be best to raise as a feature request with that author.

1 Like

Hey Dan,

Thats all good, I didn’t want to bog down our emails with Norns clock issues :upside_down_face:

Good to hear things are still in the works. Cant wait for this to be fixed properly.
I do try to keep my sync set up the same for studio to live performances which means running Norns as master isn’t ideal. I do love having a dedicated start/stop button as well as using all crow outputs for sequencing but i’ll give it a shot and see if its any better as a work around for now.

Quick question: Is sync via Link solid and can I send out clock from crow?

Your transport implementation in Cheat Codes is awesome!
Would love to see it in all scripts but I’ll make a few requests to the authors of my favourite scripts ad see what they say.

Thanks for continuing to work on this, very much appreciated :sparkles: :pray: :sparkles:

1 Like

Sync via link is super solid. Norns always takes the lead and changes the ableton clock when it first connects but no other issues. Haven’t tried crow yet in this format. I’m Just sending clock from ableton straight to eurorack via midi and to Norns.

1 Like

just a quick bump for visibility – @artfwo has done absolutely amazing work on the norns: update 210607 to address all of the issues described above. an incredible testing team (@Dewb , @license , @Quixotic7 , @anxietymachine , @proswell , @static , @midouest , @eigen , @tyleretters, @Wisdom.Water , @infinitedigits ) has put everything through its paces and it’s all come out very positively :slight_smile:

thanks to everybody for the patience + fantastic communication re: troubles over the last year – having clear repro cases has helped frame both criteria and testing :revolving_hearts:

19 Likes

Yay! Happy syncing and thanks a lot for all the work :slight_smile:

3 Likes

Can someone tell me if I’m misunderstanding clock.sync or if I’ve found a problem with it? I was seeing this in the last norns release so don’t think its related to the clock changes.

With a script:

function init()
  clock.run(step)
end

function step()
  while true do
    print(clock.get_beats())
    clock.sync(6)
  end
end

I’m expecting a wait of 6 beats each time but the first sync is always less e.g.

20444.842958333
20448.000958333
20454.000979167
20460.000333333

Every sync after that is perfect.

In my ‘In C’ script i’m playing notes in a loop like this and syncing after each note for the appropriate note length but getting unexpected results. I’ve switched to sync’ing every 0.25 beats and just looping round without playing a note if I need a longer note. That seems to work better but it would be good to understand what’s going on.

that’s the expected behavior. init() will be called at an undefined moment in time with regards to the beat coming from the clock source, and so will step(). the first thing step() does is printing current beat, which can be anything, as the clock isn’t reset on script reloading and such.

in order to do something in sync with the beat, make sure clock.sync is called before any actions that you expect to be synchronized.

it might be worth mentioning, that the absolute beat value is now returned from clock.sync after the coroutine is resumed, so the following code will also work:

function coro()
  local beat = clock.sync(x) -- wait and resume at the next Xth beat
  print(beat) -- will print the absolute beat
end
4 Likes

Thanks for the reply. I was expecting sync to wait for the specified number of beats but its waiting for the next time the beat counter is divisible by the specified number of beats. Is that right?

1 Like

20 characters of yes!

1 Like

for “wait for the specified number of beats” cases, using clock.sleep for the duration of a beat (or many beats) as measured by clock.get_beat_sec() could work:

function init()
  pending = false
end

function key(n,z)
  if z == 1 then
    if not pending then
      clock.run(play_after_x_beats,6)
      pending = true
    end
  end
end

function play_after_x_beats(x)
  clock.sync(1) -- quantizes to the nearest beat
  print("current beat: "..clock.get_beats())
  clock.sleep(clock.get_beat_sec() * x)
  print("delayed till: "..clock.get_beats())
  pending = false
end

will return:

current beat: 1830.0018375
delayed till: 1836.003975

Thanks @artfwo & @dan_derks.

I’ve been experimenting with recording midi (which is obviously the gold standard :slight_smile: ) into ableton and this pattern gave slightly better accuracy over the length of the sequence.

local waitCount = 0

while true do
  if waitCount == 0 then
	  -- turn off last note
	  -- play next note
	  waitCount = (<note length in beats> / 0.25) - 1
	else
	  waitcount = waitCount - 1
  end
  clock.sync(0.25)
end
1 Like

oh, hell yeah – i was actually writing a double-back post to mention that the code i shared is pretty literal (waiting a specified amount of time) and won’t likely perform exactly as desired in a traditional clock-sync situation where:

  • each beat coming from a leader might not an exact duration
  • we just want the things to happen at the same time, because that feels rhythmically “tight”

so yeah, an approach i’ve traditionally used is a counter-based system where a counter increments at your smallest desired beat duration and you track those for event execution, which is totally where you landed! :slight_smile: thank you for closing the loop with your results :sparkles:

artem also outlined a nice approach here, which is new to the clock improvements and leverages clock.sync()'s return of the current beat as a built-in counter method (which i need to get in the docs asap :sweat_smile: )

1 Like

with the new clock scheduler this shouldn’t be necessary, i.e. the following code:

local x = 1

clock.sync(x / 4)
if (clock.get_beats() > (x + wait_count)) then play() end
clock.sync(x / 4)
if (clock.get_beats() > (x + wait_count)) then play() end
clock.sync(x / 4)
if (clock.get_beats() > (x + wait_count)) then play() end
clock.sync(x / 4)
if (clock.get_beats() > (x + wait_count)) then play() end

wait_count = wait_count + 1

can be replaced with

local x = 1
clock.sync(x) -- guaranteed to resume at xth beat
play()

since the new scheduler follows the source very closely, tempo changes and midi signal jitter are less of a problem now.

in musical functions i recommend choosing sync over sleep for most cases, as it guarantees stable rhythms in the script with graceful handling of tempo and source changes and better transport syncing.

1 Like

with norns: update 210630 it’s possible to specify an offset for a sync position as follows:

 -- sync and resume at the next whole beat as usual
clock.sync(1)
-- sync and resume at the next whole beat with a half-beat delay
clock.sync(1, 0.5)
-- sync to the next 4th beat, but resume earlier by a quarter-beat 
clock.sync(4, -1/4)

-- create a swinging feel for a coroutine
-- by delaying even beats by half of the beat length
local beat = 1/4
local offset = beat / 2
local swing = 0

clock.run(function()
  while true do
    clock.sync(beat, offset * swing)
    -- do something
    swing = swing ~ 1
  end
end)

a bit of extra care should be taken with negative offsets, as they can lead to somewhat unexpected delays, e.g. clock.sync(2, -2.5) called at beat 0 will schedule resumption of the coroutine at beat 1.5, which is computed as 4 - 2.5, where 4 is the least possible beat divisible by the sync value of 2 which can also be scheduled in the future with an offset of -2.5 beats. hope this makes sense :slight_smile:

with positive offsets, sync will just be delayed by the time specified (in beats).

generally this allows relatively painless implementation of swinging rhythms in scripts and provides quite interesting results when different swinging patterns are used concurrently.

5 Likes

ran into a bit of an issue…
how slow can the norns BPM receive?

ran into an issue last night with Cheat Codes while trying to send it Ext MIDI Clock at 50bpm.
:stuck_out_tongue:

hope all’s well :slight_smile:

1-300bpm is the supported range — lmk if there was a cc2 issue tho in the thread and i’ll take a peek!

1 Like

Are there any big things to avoid when working with clock coroutines? I am trying to use clock.sync to synchronize 2 pattern_time patterns. Everything works fine if I’m only using one of the patterns in clock-synced mode, i.e. only one clock coroutine running at a time, but as soon as I try to use both synced to the clock (2 clock coroutines running at once) I get some very strange behavior.

I do have both patterns stored in a table, and the coroutines both access the table to modify(record/start/etc) a pattern at index n, but both should be accessing different indices and different patterns.

maybe share the code? sorry i don’t quite get it.

… actually that’s ok, it’s enough i think. i’ll try and reproduce or make a working example.

Here’s the code: orison/orison.lua at 51f95423debb6d1222dc895bd95e9b15646b5793 · evannjohnson/orison · GitHub

It’s quite messy, and I’ll try to make a distilled version to illustrate what I’m talking about. There’s a table called “patterns”, indices 1 and 2 store tables with a pattern and a couple state variables, such that you can access pattern n with “patterns[n].pattern”.

All the clock synced stuff happens in record_start_sync(), record_stop_sync(), start_sync(), stop_sync(), and reset_sync(). All but reset_sync are started in g.key. Reset_sync is started by a special event I put near the end of the patterns, it is started at the top of pattern_note().

The gist of the script is that columns 2-16 is a grid of notes you can play, column 1 is control functions, (1,3) controls pattern 1, (1,4) controls pattern 2. Holding (1,7) while pressing a pattern button clears that pattern starts recording unsynced, pressing the pattern button again stops recording and then controls stopping/starting. Holding (1,6) while pressing a pattern button does the same, but synced to the clock. You can toggle the grid flashing to the clock by pressing key2 on norns while holding (1,7).

Feel free to wait for me to give you a distilled example, my code is pretty rough.

… here’s an example of two parallel clock-driven sequences. one is a steady 16th note and the other steps between a few different sync divisions in sequence.

… does that help at all?


as far as race conditions…

well, our lua VM is single threaded, the question is can we guarantee an order of execution for clock routines that are sync’d to the same beat. that’s a good question; i’ll take a quick look and see if there is an obvious answer.

for now best, to assume that you can’t and try and avoid the possibility of race conditions. (i mean once you get into any sort of complex rhythmic relationship you probably want to assume this anyway.)

so, on the other side of it i will make a brief attempt to grok your script’s management of runtime state. we’ll see.

going on your general description - it seems fine for multiple clock threads to mutate different fields in a table. it might be a generally bad idea to have both threads adding or removing elements in the same table, but even then it depends on the method - there are ways to make it safe.

4 Likes