aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2025-03-14 23:45:49 +0000
committers-ol <s+removethis@s-ol.nu>2025-03-15 14:14:04 +0000
commit8526e533c9f8d16495c248f2fd9c0b224b267d36 (patch)
tree1862a442fef10866ef641ee465c84cf37bdcee1d
parentimplement template strings (diff)
downloadalive-8526e533c9f8d16495c248f2fd9c0b224b267d36.tar.gz
alive-8526e533c9f8d16495c248f2fd9c0b224b267d36.zip
lib: add link-time module
-rw-r--r--alv-lib/link-time.moon224
-rw-r--r--docs/gen/shim.moon2
2 files changed, 225 insertions, 1 deletions
diff --git a/alv-lib/link-time.moon b/alv-lib/link-time.moon
new file mode 100644
index 0000000..1568b49
--- /dev/null
+++ b/alv-lib/link-time.moon
@@ -0,0 +1,224 @@
+import Constant, Op, Input, T, any, sig, evt from require 'alv.base'
+al = require 'abletonlink'
+
+clock = Constant.meta
+ meta:
+ name: 'clock'
+ summary: "Create a clock source."
+ examples: { '(clock [synced] [bpm] [fps])' }
+ description: "Creates a new `link/clock~` stream.
+
+The clock event stream is an IO that triggers other operators at a fixed
+frame rate.
+
+- `synced` has to be a bool~ constant and defaults to `false`.
+- `bpm` should be a num~ stream and defaults to `120`.
+- `fps` has to be a num= constant and defaults to `60` if omitted."
+ value: class extends Op
+ setup: (inputs) =>
+ { synced, bpm, fps } = (sig.bool + -sig.num + -sig.num)\match inputs
+ super
+ synced: Input.hot synced or Constant.bool false
+ bpm: Input.hot bpm or Constant.num 120
+ fps: Input.hot fps or Constant.num 60
+
+ io: Input.hot T.bang\mk_evt!
+
+ @setup_out '~', T['link/clock']
+ @state or= {
+ link: al.create 120
+ state: al.create_session_state!
+ }
+ @state.time or= @state.link\clock_micros!
+
+ poll: =>
+ time = @state.link\clock_micros!
+ dt = time - @state.time
+ ft = 1 / @inputs.fps!
+
+ if dt >= ft and not @inputs.io.result\dirty!
+ @state.state = al.create_session_state!
+ @state.link\capture_app_session_state @state.state
+ @inputs.io.result\set true
+ true
+
+ tick: (setup) =>
+ { :link, :time, :state } = @state
+
+ if @inputs.synced\dirty!
+ link\enable @inputs.synced!
+
+ if @inputs.bpm\dirty!
+ state\set_tempo @inputs.bpm!, time
+
+ link\commit_app_session_state state
+
+ if setup or @inputs.io\dirty!
+ time = link\clock_micros!
+ dt = time - @state.time
+ @state.time = time
+ @out\set :time, :state, :dt
+
+every = Constant.meta
+ meta:
+ name: 'every'
+ summary: "Emit events regularly."
+ examples: { '(every [clock] period [phase] [evt])' }
+ description: "Emits `evt` as an event once every `period` beats.
+
+- `clock` should be a `link/clock~` stream. This argument can be omitted
+ and the stream be passed as a dynamic definition in `*clock*` instead.
+- `period` should be a num~ stream.
+- `phase` should be a num~ stream and defaults to 0.
+- `evt` can be a value of any type. It defaults to `bang`.
+- the return type will be an event stream with the same type as `evt`."
+ value: class extends Op
+ pattern = -sig['link/clock'] + sig.num + -sig.num + -sig!
+ setup: (inputs, scope) =>
+ { clock, period, phase, evt } = pattern\match inputs
+ super
+ clock: Input.hot clock or scope\get '*clock*'
+ period: Input.cold period
+ phase: Input.cold phase or Constant.num 0
+ evt: Input.cold evt or T.bang\mk_const true
+
+ @setup_out '!', @inputs.evt\type!
+
+ tick: =>
+ { :clock, :period, :phase, :evt } = @unwrap_all!
+ return if period == 0
+
+ pre = clock.state\phase_at_time clock.time - clock.dt, period
+ post = clock.state\phase_at_time clock.time, period
+
+ pre = (pre + period - phase) % period
+ post = (post + period - phase) % period
+
+ if post < pre
+ @out\set evt
+
+lfo = Constant.meta
+ meta:
+ name: 'lfo'
+ summary: "Low-frequency oscillator."
+ examples: { '(lfo [clock] period [phase] [wave])' }
+ description: "Oscillates betwen `0` and `1` once every `period` beats.
+
+- `clock` should be a `link/clock~` stream. This argument can be omitted
+ and the stream be passed as a dynamic definition in `*clock*` instead.
+- `freq` should be a num~ stream.
+- `phase` should be a num~ stream and defaults to 0.
+- `wave` selects the wave shape from one of the following:
+ - `'sin'` (the default)
+ - `'saw'`
+ - `'tri'`"
+ value: class extends Op
+ pattern = -sig['link/clock'] + sig.num + -sig.num + -sig.str
+ setup: (inputs, scope) =>
+ { clock, period, phase, wave } = pattern\match inputs
+ super
+ clock: Input.hot clock or scope\get '*clock*'
+ period: Input.cold period
+ phase: Input.cold phase or Constant.num 0
+ wave: Input.cold wave or Constant.str 'sin'
+
+ @state or= 0
+ @setup_out '~', T.num
+
+ tau = math.pi * 2
+ tick: =>
+ { :clock, :period, :phase, :wave } = @unwrap_all!
+ t = clock.state\phase_at_time clock.time, period
+ t = (t - phase) / period % 1
+
+ @out\set switch wave
+ when 'sin' then .5 + .5 * math.cos t * tau
+ when 'saw' then t
+ when 'tri' then math.abs (2*t % 2) - 1
+ else error Error 'argument', "unknown wave type '#{wave}'"
+
+ vis: => type: 'bar', bar: @.out!
+
+ramp = Constant.meta
+ meta:
+ name: 'ramp'
+ summary: "Sawtooth LFO."
+ examples: { '(ramp [clock] period [phase] [max])' }
+ description: "Ramps from `0` to `max` once every `period` beats.
+
+- `clock` should be a `time/clock!` stream. This argument can be omitted
+ and the stream be passed as a dynamic definition in `*clock*` instead.
+- `period` should be a num~ stream.
+- `phase` should be a num~ stream and defaults to 0.
+- `max` should be a num~ stream and defaults to `period` if omitted."
+ value: class extends Op
+ pattern = -sig['link/clock'] + sig.num + -sig.num + -sig.num
+ setup: (inputs, scope) =>
+ { clock, period, phase, max } = pattern\match inputs
+ super
+ clock: Input.hot clock or scope\get '*clock*'
+ period: Input.cold period
+ phase: Input.cold phase or Constant.num 0
+ max: max and Input.cold max
+
+ @setup_out '~', T.num, 0
+
+ tick: =>
+ { :clock, :period, :phase, :max } = @unwrap_all!
+ t = clock.state\phase_at_time clock.time, period
+ t = (t - phase) / period % 1
+ max or= period
+ @out\set t * max
+
+ vis: => type: 'bar', bar: @state
+
+tick = Constant.meta
+ meta:
+ name: 'tick'
+ summary: "Count ticks."
+ examples: { '(tick [clock] period [phase])' }
+ description: "Counts upwards by one every `period` beats.
+
+- `clock` should be a `link/clock~` stream. This argument can be omitted
+ and the stream be passed as a dynamic definition in `*clock*` instead.
+- `period` should be a num~ stream and defaults to 1.
+- `phase` should be a num~ stream and defaults to 0.
+- returns a `num~` stream that increases by 1 every `period`."
+ value: class extends Op
+ pattern = -sig['link/clock'] + -sig.num + -sig.num
+ setup: (inputs, scope) =>
+ { clock, period, phase } = pattern\match inputs
+ super
+ clock: Input.hot clock or scope\get '*clock*'
+ period: Input.cold period or Constant.num 0
+ phase: Input.cold phase or Constant.num 0
+
+ @setup_out '~', T.num, 0
+
+ tick: =>
+ { :clock, :period, :phase, :evt } = @unwrap_all!
+ return if period == 0
+
+ pre = clock.state\phase_at_time clock.time - clock.dt, period
+ post = clock.state\phase_at_time clock.time, period
+
+ pre = (pre + period - phase) % period
+ post = (post + period - phase) % period
+
+ if post < pre
+ @out\set @.out! + 1
+
+ vis: => type: 'event'
+
+
+Constant.meta
+ meta:
+ name: 'link-time'
+ summary: "Musical time with Ableton Link."
+
+ value:
+ :clock
+ :every
+ :lfo
+ :ramp
+ :tick
diff --git a/docs/gen/shim.moon b/docs/gen/shim.moon
index 46da0ac..e8a4f08 100644
--- a/docs/gen/shim.moon
+++ b/docs/gen/shim.moon
@@ -4,7 +4,7 @@ export require
require = do
old_require = require
- blacklist = {k, true for k in *{'losc', 'socket', 'system', 'luartmidi'}}
+ blacklist = {k, true for k in *{'losc', 'socket', 'system', 'luartmidi', 'abletonlink'}}
(mod, ...) ->
return {} if blacklist[mod]
old_require mod, ...