/
clock.lua
executable file
·392 lines (336 loc) · 11.3 KB
/
clock.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
--- clock coroutines
-- @module clock
local clock = {}
clock.threads = {}
local clock_id_counter = 1
local function new_id()
local id = clock_id_counter
clock_id_counter = clock_id_counter + 1
return id
end
--- create and start a coroutine using the norns clock scheduler.
-- @tparam function f coroutine body function
-- @param[opt] ... any extra arguments will be passed to the body function
-- @treturn integer coroutine handle that can be used with clock.cancel
-- @see clock.cancel
clock.run = function(f, ...)
local coro = coroutine.create(f)
local coro_id = new_id()
clock.threads[coro_id] = coro
clock.resume(coro_id, ...)
return coro_id
end
--- stop execution of a coroutine previously started using clock.run.
-- @tparam integer coro_id coroutine handle
-- @see clock.run
clock.cancel = function(coro_id)
_norns.clock_cancel(coro_id)
clock.threads[coro_id] = nil
end
local SCHEDULE_SLEEP = 0
local SCHEDULE_SYNC = 1
--- suspend execution of the calling coroutine and schedule resuming in specified time.
-- must be called from a coroutine previously started using clock.run.
-- @tparam float s seconds to wait for
clock.sleep = function(...)
return coroutine.yield(SCHEDULE_SLEEP, ...)
end
--- suspend execution of the calling coroutine and schedule resuming at the next sync quantum of the specified value.
-- must be called from a coroutine previously started using clock.run.
-- @tparam float beat sync quantum. may be larger than 1
-- @tparam[opt] float offset if set, this value will be added to the sync quantum
clock.sync = function(...)
return coroutine.yield(SCHEDULE_SYNC, ...)
end
-- todo: use c api instead
clock.resume = function(coro_id, ...)
local coro = clock.threads[coro_id]
local result, mode, time, offset = coroutine.resume(coro, ...)
if coroutine.status(coro) == "dead" then
if result then
clock.cancel(coro_id)
else
error(mode)
end
else
-- not dead
if result and mode ~= nil then
if mode == SCHEDULE_SLEEP then
_norns.clock_schedule_sleep(coro_id, time)
elseif mode == SCHEDULE_SYNC then
_norns.clock_schedule_sync(coro_id, time, offset)
else
error('invalid clock scheduler mode')
end
end
end
end
clock.cleanup = function()
for id, coro in pairs(clock.threads) do
if coro then
clock.cancel(id)
end
end
clock.transport.start = nil
clock.transport.stop = nil
clock.tempo_change_handler = nil
end
--- select the sync source.
-- @tparam string source "internal", "midi", or "link"
clock.set_source = function(source)
if type(source) == "number" then
_norns.clock_set_source(util.clamp(source-1,0,3)) -- lua list is 1-indexed
elseif source == "internal" then
_norns.clock_set_source(0)
elseif source == "midi" then
_norns.clock_set_source(1)
elseif source == "link" then
_norns.clock_set_source(2)
else
error("unknown clock source: "..source)
end
end
--- get current time in beats.
clock.get_beats = function()
return _norns.clock_get_time_beats()
end
--- get current tempo.
clock.get_tempo = function()
return _norns.clock_get_tempo()
end
--- get length of a single beat at current tempo in seconds.
clock.get_beat_sec = function()
return 60.0 / clock.get_tempo()
end
clock.transport = {}
--- static callback when clock transport is started;
-- user scripts can redefine
-- @static
clock.transport.start = nil
--- static callback when clock transport is stopped;
-- user scripts can redefine
-- @static
clock.transport.stop = nil
--- static callback when clock tempo is adjusted via PARAMETERS > CLOCK > tempo;
-- user scripts can redefine
-- @static
-- @param bpm : the new tempo
clock.tempo_change_handler = nil
clock.internal = {}
clock.internal.set_tempo = function(bpm)
return _norns.clock_internal_set_tempo(bpm)
end
clock.internal.start = function()
return _norns.clock_internal_start()
end
clock.internal.stop = function()
return _norns.clock_internal_stop()
end
clock.crow = {}
clock.crow.in_div = function(div)
_norns.clock_crow_in_div(div)
end
clock.midi = {}
clock.link = {}
clock.link.set_tempo = function(bpm)
return _norns.clock_link_set_tempo(bpm)
end
clock.link.set_quantum = function(quantum)
return _norns.clock_link_set_quantum(quantum)
end
clock.link.start = function()
return _norns.clock_link_set_transport_start()
end
clock.link.stop = function()
return _norns.clock_link_set_transport_stop()
end
clock.link.set_start_stop_sync = function(enabled)
return _norns.clock_link_set_start_stop_sync(enabled)
end
_norns.clock.start = function()
if clock.transport.start ~= nil then
clock.transport.start()
end
end
_norns.clock.stop = function()
if clock.transport.stop ~= nil then
clock.transport.stop()
end
end
function clock.add_params()
local send_midi_clock = {}
params:add_group("CLOCK", 29)
params:add_option("clock_source", "source", {"internal", "midi", "link", "crow"},
norns.state.clock.source)
params:set_action("clock_source",
function(x)
if x==3 then clock.link.set_tempo(params:get("clock_tempo")) end -- for link, apply tempo before setting source
clock.set_source(x)
if x==4 then
norns.crow.clock_enable()
end
norns.state.clock.source = x
if x==1 then clock.internal.set_tempo(params:get("clock_tempo")) end
end)
params:set_save("clock_source", false)
params:add_number("clock_tempo", "tempo", 1, 300, norns.state.clock.tempo)
params:set_action("clock_tempo",
function(bpm)
local source = params:string("clock_source")
if source == "internal" then clock.internal.set_tempo(bpm)
elseif source == "link" then clock.link.set_tempo(bpm) end
norns.state.clock.tempo = bpm
if clock.tempo_change_handler ~= nil then
clock.tempo_change_handler(bpm)
end
end)
params:set_save("clock_tempo", false)
params:add_trigger("clock_reset", "reset")
params:set_action("clock_reset",
function()
local source = params:string("clock_source")
if source == "internal" then clock.internal.start()
elseif source == "link" then print("link reset not supported") end
end)
params:add_separator("link_separator", "link")
params:add_number("link_quantum", "link quantum", 1, 32, norns.state.clock.link_quantum)
params:set_action("link_quantum",
function(x)
clock.link.set_quantum(x)
norns.state.clock.link_quantum = x
end)
params:set_save("link_quantum", false)
params:add_option("link_start_stop_sync", "link start/stop sync", {"disabled", "enabled"}, norns.state.clock.link_start_stop_sync)
params:set_action("link_start_stop_sync",
function(x)
clock.link.set_start_stop_sync(x == 2)
norns.state.clock.link_start_stop_sync = x
end)
params:set_save("link_start_stop_sync", false)
params:add_separator("midi_clock_out_separator", "midi clock out")
for i = 1,16 do
local short_name = string.len(midi.vports[i].name) <= 20 and midi.vports[i].name or util.acronym(midi.vports[i].name)
params:add_binary("clock_midi_out_"..i, i..". "..short_name, "toggle", norns.state.clock.midi_out[i])
params:set_action("clock_midi_out_"..i,
function(x)
if x == 1 then
if not tab.contains(send_midi_clock,i) then
table.insert(send_midi_clock,i)
end
else
if tab.contains(send_midi_clock,i) then
table.remove(send_midi_clock,tab.key(send_midi_clock, i))
end
end
norns.state.clock.midi_out[i] = x
end
)
if short_name ~= "none" and midi.vports[i].connected then
params:show("clock_midi_out_"..i)
else
params:hide("clock_midi_out_"..i)
end
params:set_save("clock_midi_out_"..i, false)
end
params:add_separator("midi_clock_in_separator", "midi clock in")
local midi_clock_in_options = { "all", "none"}
for i=1,16 do
table.insert(midi_clock_in_options, tostring(i))
end
params:add_option("clock_midi_in", "midi clock in",
midi_clock_in_options, norns.state.clock.midi_in)
params:set_action("clock_midi_in", function(x)
norns.state.clock.midi_in = x
midi.update_clock_receive()
end)
params:set_save("clock_midi_in", false)
params:add_separator("crow_clock_separator", "crow")
params:add_option("clock_crow_out", "crow out",
{"off", "output 1", "output 2", "output 3", "output 4"}, norns.state.clock.crow_out)
params:set_action("clock_crow_out", function(x)
--if x>1 then crow.output[x-1].action = "pulse(0.01,8)" end
norns.state.clock.crow_out = x
end)
params:set_save("clock_crow_out", false)
params:add_number("clock_crow_out_div", "crow out div", 1, 32,
norns.state.clock.crow_out_div)
params:set_action("clock_crow_out_div",
function(x) norns.state.clock.crow_out_div = x end)
params:set_save("clock_crow_out_div", false)
params:add_number("clock_crow_in_div", "crow in div", 1, 32,
norns.state.clock.crow_in_div)
params:set_action("clock_crow_in_div",
function(x)
clock.crow.in_div(x)
norns.state.clock.crow_in_div = x
end)
params:set_save("clock_crow_in_div", false)
--params:add_trigger("crow_clear", "crow clear")
--params:set_action("crow_clear",
--function() crow.reset() crow.clear() end)
params:bang("clock_tempo")
-- executes crow sync
clock.run(function()
while true do
clock.sync(1/params:get("clock_crow_out_div"))
local crow_out = params:get("clock_crow_out")-1
if crow_out > 0 then
crow.output[crow_out].volts = 10
clock.sleep(60/(2*clock.get_tempo()*params:get("clock_crow_out_div")))
crow.output[crow_out].volts = 0
end
end
end)
-- executes midi out (needs a subtick)
clock.run(function()
while true do
clock.sync(1/24)
for i = 1,#send_midi_clock do
local port = send_midi_clock[i]
midi.vports[port]:clock()
end
end
end)
-- update tempo param value
clock.run(function()
while true do
if params:get("clock_source") ~= 1 then
local external_tempo = math.floor(clock.get_tempo() + 0.5)
local previous_val = params:get("clock_tempo")
params:set("clock_tempo", external_tempo, true)
if clock.tempo_change_handler ~= nil and previous_val ~= external_tempo then
clock.tempo_change_handler(external_tempo)
end
end
clock.sleep(1)
end
end)
end
clock.help = [[
--------------------------------------------------------------------------------
clock.run( func ) start a new coroutine with function [func]
(returns) created id
clock.cancel( id ) cancel coroutine [id]
clock.sleep( time ) resume in [time] seconds
clock.sync( beats ) resume at next sync quantum of value [beats]
following to global tempo
clock.get_beats() (returns) current time in beats
clock.get_tempo() (returns) current tempo
clock.get_beat_sec() (returns) length of a single beat at current
tempo in seconds
--------------------------------------------------------------------------------
-- example
-- start a clock with calling function [loop]
function init()
clock.run(loop)
end
-- this function loops forever, printing at 1 second intervals
function loop()
while true do
print("so true")
clock.sleep(1)
end
end
--------------------------------------------------------------------------------
]]
return clock