monome / crow

Crow speaks and listens and remembers bits of text. A scriptable USB-CV-II machine
GNU General Public License v3.0
166 stars 34 forks source link

Norns 'clock' module support on crow (global timing) #339

Closed trentgill closed 3 years ago

trentgill commented 4 years ago

Add norns 'clock' module support to crow.

This allows for a variety of clock synchronization functions, and introduces the notion of a global clock on crow.

While the implementation will be substantially different from the norns version (using a single hardware timer for the clock base instead), it should present the same interface to the user.

extended features

timeline support. ie a table of time, function pairs that will be executed. useful for 'composition' style sequencing.

trentgill commented 4 years ago

The above notes aren't really relevant to the actual solution. Open PR #352 solves this issue by directly translating the norns implementation to using the system clock on crow. The timing granularity is only 1ms so there is more jitter than on norns, but in general, it should be sufficient in all but the most extreme situations.

There is an issue where the resumption of the clock routine is delayed slightly (by the event system), causing a subsequent clock.sync to miss a beat, or a clock.sleep to accumulate these small delays in a while loop. Could be solved by saving the timestamp of an event callback inside the clock's struct -- then, if that coroutine is re-entered within 1ms of it being resumed (ie assume it has called into lua to process the next code up to a clock.* event), use the saved timestamp as the reference point, rather than the 'now time' so events are registered at the right moment. (?)

clock sources

The main issue to be discussed is relating to clock-sources. Which ones, and how they should relate to normal crow usage:

clock.internal

This is the currently implemented clock source and seems to be working well. It's just an internal timer that accumulates over time. Interface is:

-- currently working
clock.set_source( 'internal' )
clock.internal.set_tempo( 160 )

clock.crow

Like on norns, this uses the crow input jacks as the clock source. Implemented in the same way with smoothing and clock divider. When crow is being clocked by the input jack there's no real need for the input jack to be accessible to a crow script, because you can use a clock routine with clock.sync(1) to create the same effect.

clock.set_source( 'crow', 1 ) -- awkward calling it 'crow' on crow
clock.set_source( input[1] ) -- requires some metatable magic

It makes more sense for the user not to have the change event callback happen when the input is used to drive the clock. This actually makes the implementation really simple and has the benefit of reducing clock latency & jitter (the clock is driven direct from C, rather than via the lua VM).

clock.midi

TBD. This should be figured out once we have the USB MIDI working well, but will likely be a key usage of the MIDI connection (ie. to use crow to send MIDI synchronized clocks (whether with clock divs or swing etc).

clock actions

On norns, most of the clock interaction is handled by the menu system. setting tempo / reset etc is all done with a standardized menu, rather than from a script. obviously on crow we don't have a UI beyond the CV jacks, so any interface must be defined in-script. As such it makes sense to talk about what features are important to add to the documented API.

clock.set_source( source ) -- 'internal', 'crow', 'midi', 'norns'
clock.reset() -- for 'internal' resets counter & restarts immediately, for 'crow' resets to count = 1 on next tick

clock.output

Options: crow, norns, or midi.

clock.output.norns

When norns selects 'crow' as it's input device, that should tell crow to select clock source as input 1, and output as norns. This should perhaps not be a crow option? It will have no effect until norns selects the crow-input option, and when attached to norns, typically folks will see crow as a dumb expander (ie it should take directions, rather than send them to norns).

For voltage output, this should likely be user-implemented as a clock routine over the output:

-- directly set it up
clock.run(
  function()
    clock.sync(1)
    output[1]( pulse() )
  end)

-- can provide a helper function
function clock.crow.output( channel, sync )
  return clock.run(
    function()
      clock.sync(sync)
      output[channel]( pulse() )
    end)
end

-- so user can call:
clock.crow.output( 1, 1/4 ) -- set output 1 to pulse every quarter beat

-- when norns sets 'crow out div $divs' it just calls
clock.crow.output( 1, 1/divs )

sending midi should work the same as norns just needs some lua syntax

clock.midi.output( chan )
  return clock.run(
    function()
      clock.sync(1/24)
      -- TODO send MIDI clock signal. see midi issue
    end)
end
trentgill commented 4 years ago

@tehn would love your input and thoughts about this if you have time!

tehn commented 4 years ago

on it today!

On Fri, Jul 24, 2020, 3:19 PM trent notifications@github.com wrote:

@tehn https://github.com/tehn would love your input and thoughts about this if you have time!

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/monome/crow/issues/339#issuecomment-663693658, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB4I4GXGMAIS4CJ5WAVRELR5HNFJANCNFSM4OFEM3ZA .

tehn commented 4 years ago

Could be solved by saving the timestamp of an event callback inside the clock's struct -- then, if that coroutine is re-entered within 1ms of it being resumed (ie assume it has called into lua to process the next code up to a clock.* event), use the saved timestamp as the reference point, rather than the 'now time' so events are registered at the right moment. (?)

this is a very good solution in my opinion

clock.set_source( 'crow', 1 ) -- awkward calling it 'crow' on crow

i propose

clock.set_source('input',1)

i agree that setting an input as clock source should otherwise disable other operations. i guess they should still be able to query pin state... i doubt that's trouble though.

clock.midi

TBD (wait for the midi code first!) :)

but it should be straightforward. the bytes are fixed with a specific timing division. (i did the midi clock norns code so i can clarify later)

clock actions ---> On norns, most of the clock interaction is handled by the menu system

this is actually a good issue to add to norns as well: ie, clock.set_tempo() should exist and do the branching based on clock source, rather than have that code be within the param callback (of course, on crow there is only one thing you can set tempo for, internal--- but simplifying the syntax would be great.) same for reset.

see https://github.com/monome/norns/blob/main/lua/core/clock.lua#L196 for list of stuff norns menu items do.

i guess input_div is good when input is the source (norns does this)

clock.output

the norns option is a little complex:

-- so user can call: clock.crow.output( 1, 1/4 ) -- set output 1 to pulse every quarter beat

-- when norns sets 'crow out div $divs' it just calls clock.crow.output( 1, 1/divs )

this seems nice, but i think we'll have to save the clock as to cancel it before changing the value... because i see people wanting to dynamically change the subdivisions of a running output clock.

i do agree that this part seems a little extra hand-holdy perhaps at the expense of flexibility (ie, actually just learning how to use clocks and do something more interesting than a 3-line script which does clock division!)

the midi output is helpful, though. needs enable/disable and channel.

norns clock output should have enable/disable and div. (i guess div can also be a fraction, becoming a mul)


happy to clarify or revisit ideas here!

trentgill commented 3 years ago

Coming back to this issue. I've spent some time on the clocks branch and it's generally working quite well with internal clock generation.

I just read back through the previous discussion and i've had some alternative ideas for how to implement some of the helper functionality, and how to refer to clock-source switching:

For clock-output to a CV jack, i'm thinking we should add a helper fn to the output[n] table, which leverages clock, rather than having it as a mode of clock. This feels more natural to me as all of crow speaks about outputs directly, rather than calling fns that affect the outputs. eg. ASL is a complex library that is a function of output. I've thinking:

-- example usage
output[1]:clock(div) -- where div is essentially the argument to `clock.sync()`

-- Output library code
function Output.clock(self, div)
    self.clock_div = div or self.clock_div
    if self._clock then clock.cancel(self._clock) end -- replace the existing coro
    self._clock = clock.run(function()
            while true do
                clock.sync(self.clock_div)
                self.asl:action()
            end
        end)
end

Basically it's an inversion of control: Output leverages Clock to extend the functionality of Output (as opposed to the prior: Clock takes control of Output).


Similarly I want to approach clock_source in the same way. Specifically focusing on CV input for controlling the global clock:

-- set input mode to control global clock
input[1].mode('clock',div, avg) -- div should equate input rate with clock.sync() rate. avg is number of clocks to smooth over

Here we use the change dsp under the hood, but don't create a user event on each detected trigger. Instead these events are captured in C and forwarded directly to the clock library. Also, when setting the input mode to clock, we set clock.source = input[n] automatically. In spite of there being no user event, creating a clock at sync(1) is directly equivalent to the change callback:

-- input event only
input[1].mode('change')
input[1].change = do_thing

-- input drives clock, and creates events on each input pulse
input[1].mode('clock')
clock.run(function()
  while true
    clock.sync(1)
    do_thing() -- should work identically to the above event
  end
end)

There is some design questions about how to handle source switching (eg. what happens when input.mode'clock' is set, and then disabled? what happens when both inputs are set to 'clock'?), but i think these are secondary concerns.

The primary point being (again) that controlling what the inputs do, should be handled by the Input library, not the Clock library.


I know @tehn 's last message spoke about not focusing on a 3-line clock-divider script, but i think there is a benefit to making these timed events nearly effortless -- practically it provides 'clock division' as a first-class functionality, which means building more complex actions on top of clock mul/div is far easier & there's little boilerplate. Also, clock-multiplication is something people love to ask for, but nobody has provided a solid re-usable implementation. Clocks are a great solution.

Anyway, here's the 4 line clock-divider script:

function init()
  input[1].mode 'clock'
  output[1]:clock(2) -- where 2 is the division
end

-- can update the output clock_div on the fly:
output[1].clock_div = 4

Which gets me thinking about writing a simple quantization script, useful for working with decorrelated trigger streams:

-- input[1] is timebase
-- input[2] is pulse-train to be quantized to the timebase

QUANT = 1/4 -- quantize to 4segments per beat

function init()
  input[1].mode 'clock'
  input[2].mode 'change'
  input[2].change = quantize_event
  output[1].action = pulse()
end

function quantize_event(dir)
  clock.run(function()
    clock.sync(QUANT) -- wait until next QUANT tick
    if not dir then clock.sleep(0.01) end -- delay noteoff by 10ms to avoid on/off falling into same window
    output[1]() -- pulse the output
  end)
end

So the last category of functionality (and the one i'm least confident designing) is interaction with a usb host. Specifically norns, and M4L. When sending clocks out of crow, driven from the host, I actually don't think clocks are necessary and we don't need to support following the clock. The specific reason being that most existing scripts take the approach of sending explicit commands on every event (rather than a description of when to send events in the future). I think that is absolutely fine and echoes existing note generation style (eg supercollider engines in norns, midi-notes in m4l).

When the host is being clocked from crow's input, we currently just use the default input[n].mode 'change' event handler as the clock base. I think this approach is generally ok, but we can improve it by adding a new mode input[n].mode 'clock+' or similar. Basically identical to the above-described 'clock' input mode, but it also sends an event over TTY for sync purposes. Because the event is fixed, it can be handled in C, so the timing accuracy is improved. Also it means the crow will have its clock locked to the same timebase, so it's easy to enable clock divided outs, or other tempo-oriented functions.

I guess this means the upstream clock handling is mostly unchanged, we just use a new input mode. It would be easy to first set 'change' mode such that if an older version of crow was running with new norns/m4l, it would fall back to the current solution.


would love any feedback on the above!

tehn commented 3 years ago

i am hugely in favor of all of your points here! (particularly being able to optimize within the C layer.) please disregard my earlier assessment from last july.

re: multiple clock inputs. is it worth even supporting clock from both jacks? i guess it's maybe interesting to have clock input streams in both jacks and being able to dynamically (via script) switch between jack sources... so yes? my impulse assumption:

re new clock input mode (sending via usb) i am all for some minor breaking changes to improve the norns-crow sync optimization (ie clock+). maybe just add a flag somewhere for enable/disable send TTY sync?

trentgill commented 3 years ago

fixed in #393

opening a new issue to handle different input sources