13
0
livetrax/share/scripts/barlow_arp.lua

752 lines
25 KiB
Lua
Raw Normal View History

ardour {
["type"] = "dsp",
name = "Arpeggiator (Barlow)",
category = "Effect",
author = "Albert Gräf",
license = "GPL",
description = [[barlow_arp v0.3
Simple monophonic arpeggiator example with sample-accurate triggering and velocities computed using Barlow's indispensability formula. This automatically adjusts to the current time signature and division to produce rhythmic accents in accordance with the meter by varying the note velocities in a given range.
In memory of Clarence Barlow (27 December 1945 29 June 2023).
]]
}
-- Copyright (c) 2023 Albert Gräf, GPLv3+
-- This is basically the same as simple_arp.lua (which see), but computes note
-- velocities using the Barlow indispensability formula which produces more
-- detailed rhythmic accents and handles arbitrary time signatures with ease.
-- It also offers a pulse filter which lets you filter notes by normalized
-- pulse strengths. Any pulse with a strength below/above the given
-- minimum/maximum values in the 0-1 range will be skipped. In this case you
-- can set the gate value to 0 a.k.a. "legato" to have notes extend over
-- skipped steps until the next note arrives. This only makes an audible
-- difference if the pulse filter is in effect, otherwise a gate value of 0
-- has effectively the same meaning as 1.
-- NOTE: A limitation of the present algorithm is that only subdivisions <= 7
-- (a.k.a. septuplets) are supported, but if you really need more, then you
-- may also just change the time signature accordingly. Also, there's no swing
-- control, but you can easily get a triplet feel with the pulse filter
-- instead (e.g., in 4/4 try a triplet division along with a minimum pulse
-- strength of 0.3).
function dsp_ioconfig ()
return { { midi_in = 1, midi_out = 1, audio_in = -1, audio_out = -1}, }
end
function dsp_options ()
return { time_info = true }
end
function dsp_params ()
return
{
{ type = "input", name = "Division", min = 1, max = 7, default = 1, integer = true, doc = "number of subdivisions of the beat" },
{ type = "input", name = "Octave up", min = 0, max = 5, default = 0, integer = true, doc = "octave range up" },
{ type = "input", name = "Octave down", min = 0, max = 5, default = 0, integer = true, doc = "octave range down" },
{ type = "input", name = "Pattern", min = 1, max = 6, default = 1, integer = true, doc = "pattern style",
scalepoints =
{ ["1 up"] = 1, ["2 down"] = 2, ["3 exclusive"] = 3, ["4 inclusive"] = 4, ["5 order"] = 5, ["6 random"] = 6 } },
{ type = "input", name = "Min Velocity", min = 0, max = 127, default = 60, integer = true, doc = "minimum velocity" },
{ type = "input", name = "Max Velocity", min = 0, max = 127, default = 120, integer = true, doc = "maximum velocity" },
{ type = "input", name = "Min Filter", min = 0, max = 1, default = 0, doc = "minimum pulse strength" },
{ type = "input", name = "Max Filter", min = 0, max = 1, default = 1, doc = "maximum pulse strength" },
{ type = "input", name = "Latch", min = 0, max = 1, default = 0, toggled = true, doc = "toggle latch mode" },
{ type = "input", name = "Sync", min = 0, max = 1, default = 0, toggled = true, doc = "toggle sync mode" },
{ type = "input", name = "Bypass", min = 0, max = 1, default = 0, toggled = true, doc = "bypass the arpeggiator, pass through input notes" },
{ type = "input", name = "Gate", min = 0, max = 1, default = 1, doc = "gate as fraction of pulse length", scalepoints = { legato = 0 } },
}
end
function presets()
-- just a few basic examples for now, we'll add more stuff here later
return
{
{ name = "0 default", params = { Division = 1, ["Octave up"] = 0, ["Octave down"] = 0, Pattern = 1, ["Min Velocity"] = 60, ["Max Velocity"] = 120, ["Min Filter"] = 0, ["Max Filter"] = 1, Latch = 0, Sync = 0, Gate = 1 } },
{ name = "1 latch", params = { Latch = 1, Sync = 0 } },
{ name = "2 latch and sync", params = { Latch = 1, Sync = 1 } },
{ name = "3 bass", params = { Division = 1, ["Octave up"] = 0, ["Octave down"] = 1, Pattern = 1, ["Min Filter"] = 0, ["Max Filter"] = 1, Gate = 1 } },
{ name = "4 triplet feel #1 - synth", params = { Division = 3, ["Octave up"] = 1, ["Octave down"] = 1, Pattern = 3, ["Min Filter"] = 0.2, ["Max Filter"] = 1, Gate = 0 } },
{ name = "5 triplet feel #2 - drums", params = { Division = 3, ["Octave up"] = 0, ["Octave down"] = 0, Pattern = 1, ["Min Filter"] = 0.2, ["Max Filter"] = 1, Gate = 0 } },
}
end
-- debug level (1: print beat information in the log window, 2: also print the
-- current pattern whenever it changes, 3: also print note information, 4:
-- print everything)
local debug = 0
local chan = 0 -- MIDI output channel
local last_rolling -- last transport status, to detect changes
local last_beat -- last beat number
local last_num -- last note
local last_chan -- MIDI channel of last note
local last_gate -- off time of last note
local last_up, last_down, last_mode, last_sync, last_bypass -- previous params, to detect changes
local chord = {} -- current chord (note store)
local chord_index = 0 -- index of last chord note (0 if none)
local latched = {} -- latched notes
local pattern = {} -- current pattern
local index = 0 -- current pattern index (reset when pattern changes)
-- Meter object
Meter = {}
Meter.__index = Meter
function Meter:new(m) -- constructor
-- n = maximum subdivision, septoles seem to work reasonably well
-- meter = meter, {4} a.k.a. common time is default
-- indisp = indispensability tables, computed below
local x = setmetatable({ n = 7, meter = {4}, indisp = {} }, Meter)
x:compute(m)
return x
end
-- Computes the best subdivision q in the range 1..n and pulse p in the range
-- 0..q so that p/q matches the given phase f in the floating point range 0..1
-- as closely as possible. Returns p, q and the absolute difference between f
-- and p/q. NB: Seems to work best for q values up to 7.
local function subdiv(n, f)
local best_p, best_q, best = 0, 0, 1
for q = 1, n do
local p = math.floor(f*q+0.5) -- round towards nearest pulse
local diff = math.abs(f-p/q)
if diff < best then
best_p, best_q, best = p, q, diff
end
end
return best_p, best_q, best
end
-- prime factors of integers
local function factor(n)
local factors = {}
if n<0 then n = -n end
while n % 2 == 0 do
table.insert(factors, 2)
n = math.floor(n / 2)
end
local p = 3
while p <= math.sqrt(n) do
while n % p == 0 do
table.insert(factors, p)
n = math.floor(n / p)
end
p = p + 2
end
if n > 1 then -- n must be prime
table.insert(factors, n)
end
return factors
end
-- reverse a table
local function reverse(list)
local res = {}
for k, v in ipairs(list) do
table.insert(res, 1, v)
end
return res
end
-- arithmetic sequences
local function seq(from, to, step)
step = step or 1;
local sgn = step>=0 and 1 or -1
local res = {}
while sgn*(to-from) >= 0 do
table.insert(res, from)
from = from + step
end
return res
end
-- some functional programming goodies
local function map(list, fn)
local res = {}
for k, v in ipairs(list) do
table.insert(res, fn(v))
end
return res
end
local function reduce(list, acc, fn)
for k, v in ipairs(list) do
acc = fn(acc, v)
end
return acc
end
local function collect(list, acc, fn)
local res = {acc}
for k, v in ipairs(list) do
acc = fn(acc, v)
table.insert(res, acc)
end
return res
end
local function sum(list)
return reduce(list, 0, function(a,b) return a+b end)
end
local function prd(list)
return reduce(list, 1, function(a,b) return a*b end)
end
local function sums(list)
return collect(list, 0, function(a,b) return a+b end)
end
local function prds(list)
return collect(list, 1, function(a,b) return a*b end)
end
-- indispensabilities (Barlow's formula)
local function indisp(q)
function ind(q, k)
-- prime indispensabilities
function pind(q, k)
function ind1(q, k)
local i = ind(reverse(factor(q-1)), k)
local j = i >= math.floor(q / 4) and 1 or 0;
return i+j
end
if q <= 3 then
return (k-1) % q
elseif k == q-2 then
return math.floor(q / 4)
elseif k == q-1 then
return ind1(q, k-1)
else
return ind1(q, k)
end
end
local s = prds(q)
local t = reverse(prds(reverse(q)))
return
sum(
map(seq(1, #q),
function(i)
return s[i] *
pind(q[i], (math.floor((k-1) % t[1] / t[i+1]) + 1) % q[i])
end
))
end
if type(q) == "number" then
q = factor(q)
end
if type(q) ~= "table" then
error("invalid argument, must be an integer or table of primes")
else
return map(seq(0,prd(q)-1), function(k) return ind(q,k) end)
end
end
local function tableconcat(t1,t2)
local res = {}
for i=1,#t1 do
table.insert(res, t1[i])
end
for i=1,#t2 do
table.insert(res, t2[i])
end
return res
end
-- This optionally takes a new meter as argument and (re)computes the
-- indispensability tables. NOTE: This can be called (and the meter be
-- changed) at any time.
function Meter:compute(meter)
meter = meter or self.meter
-- a number is interpreted as a singleton list
meter = type(meter) == "number" and {meter} or meter
self.meter = meter
local n = 1
local m = {}
for i,q in ipairs(meter) do
if q ~= math.floor(q) then
error("meter: levels must be integer")
elseif q < 1 then
error("meter: levels must be positive")
end
-- factorize each level as Barlow's formula assumes primes
m = tableconcat(m, factor(q))
n = n*q
end
self.beats = n
self.last_q = nil
if self.beats > 1 then
self.indisp[1] = indisp(m)
for q = 2, self.n do
local qs = tableconcat(m, factor(q))
self.indisp[q] = indisp(qs)
end
else
self.indisp[1] = {0}
for q = 2, self.n do
self.indisp[q] = indisp(q)
end
end
end
-- This takes the (possibly fractional) pulse and returns the pulse strength
-- along with the total number of beats.
function Meter:pulse(f)
if type(f) ~= "number" then
error("meter: beat index must be a number")
elseif f < 0 then
error("meter: beat index must be nonnegative")
end
local beat, f = math.modf(f)
-- take the beat index modulo the total number of beats
beat = beat % self.beats
if self.n > 0 then
local p, q = subdiv(self.n, f)
if self.last_q then
local x = self.last_q / q
if math.floor(x) == x then
-- If the current best match divides the previous one, stick to
-- it, in order to prevent the algorithm from quickly changing
-- back to the root meter at each base pulse. XXFIXME: This may
-- stick around indefinitely until the meter changes. Maybe we'd
-- rather want to reset this automatically after some time (such
-- as a complete bar without non-zero phases)?
p, q = x*p, x*q
end
end
self.last_q = q
-- The overall zero-based pulse index is beat*q + p. We add 1 to
-- that to get a 1-based index into the indispensabilities table.
local w = self.indisp[q][beat*q+p+1]
return w, self.beats*q
else
local w = self.indisp[1][beat+1]
return w, self.beats
end
end
-- NOTE: Computing the necessary tables for the Barlow meter is a fairly
-- cpu-intensive operation, so changing the time signature mid-flight might
-- cause some cpu spikes and thus x-runs. To mitigate this, we cache each
-- meter as soon as we first encounter it, so that no costly recomputations
-- are needed later. An initial scan of the timeline makes sure that the cache
-- is well-populated from the get-go.
local last_mdiv
-- cached Barlow meters
local barlow_meters = { [4] = Meter:new() } -- common time
-- current Barlow meter
local barlow_meter = barlow_meters[4]
function dsp_init (rate)
local loc = Session:locations():session_range_location()
if loc then
local tm = Temporal.TempoMap.read ()
local a, b = loc:start():beats(), loc:_end():beats()
if debug >= 1 then
print(loc:name(), a, b)
end
-- Scan through the timeline to find all time signatures and cache the
-- resulting Barlow meters. Note that only care about the number of
-- divisions here, that's all the algorithm needs.
while a <= b do
local m = tm:meter_at_beats(a)
local mdiv = m:divisions_per_bar()
if not barlow_meters[mdiv] then
if debug >= 1 then
print(a, string.format("%d/%d", mdiv, m:note_value()))
end
barlow_meters[mdiv] = Meter:new(mdiv)
end
a = a:next_beat()
end
elseif debug >= 1 then
print("empty session")
end
end
function dsp_run (_, _, n_samples)
assert (type(midiout) == "table")
assert (type(time) == "table")
assert (type(midiout) == "table")
local ctrl = CtrlPorts:array ()
-- We need to make sure that these are integer values. (The GUI enforces
-- this, but fractional values may occur through automation.)
local subdiv, up, down, mode = math.floor(ctrl[1]), math.floor(ctrl[2]), math.floor(ctrl[3]), math.floor(ctrl[4])
local minvel, maxvel = math.floor(ctrl[5]), math.floor(ctrl[6])
-- these are floating point values in the 0-1 range
local minw, maxw = ctrl[7], ctrl[8]
local gate = ctrl[12]
-- latch toggle
local latch = ctrl[9] > 0
-- sync toggle
local sync = ctrl[10] > 0
-- bypass toggle
local bypass = ctrl[11] > 0
-- rolling state: It seems that we need to check the transport state (as
-- given by Ardour's "transport finite state machine" = TFSM) here, even if
-- the transport is not actually moving yet. Otherwise some input notes may
-- errorneously slip through before playback really starts.
local rolling = Session:transport_state_rolling ()
-- whether the pattern must be recomputed, due to parameter changes or MIDI
-- input
local changed = false
if up ~= last_up or down ~= last_down or mode ~= last_mode then
last_up = up
last_down = down
last_mode = mode
changed = true
end
if sync ~= last_sync then
last_sync = sync
index = 0
end
if not latch and next(latched) ~= nil then
latched = {}
changed = true
end
local all_notes_off = false
if bypass ~= last_bypass then
last_bypass = bypass
all_notes_off = true
end
if last_rolling ~= rolling then
last_rolling = rolling
-- transport change, send all-notes off (we only do this when transport
-- starts rolling, to silence any notes that may have been passed
-- through beforehand; note that Ardour automatically sends
-- all-notes-off to all MIDI channels anyway when transport is stopped)
if rolling then
all_notes_off = true
end
end
local k = 1
if all_notes_off then
--print("all-notes-off", chan)
midiout[k] = { time = 1, data = { 0xb0+chan, 123, 0 } }
k = k+1
end
for _,ev in ipairs (midiin) do
local status, num, val = table.unpack(ev.data)
local ch = status & 0xf
status = status & 0xf0
if not rolling or bypass then
-- arpeggiator is just listening, pass through all MIDI data
midiout[k] = ev
--print(string.format("[%d] %0x %d %d", ev.time, ev.data[1], ev.data[2], ev.data[3]))
k = k+1
elseif status >= 0xb0 then
-- arpeggiator is playing, pass through all MIDI data that's not
-- note-related, i.e., control change, program change, channel
-- pressure, pitch wheel, and system messages
midiout[k] = ev
k = k+1
end
if status == 0x80 or status == 0x90 and val == 0 then
if debug >= 4 then
print("note off", num, val)
end
-- keep track of latched notes
if latch then
latched[num] = chord[num]
else
changed = true
end
chord[num] = nil
elseif status == 0x90 then
if debug >= 4 then
print("note on", num, val, "ch", ch)
end
if latch and next(chord) == nil then
-- new pattern, get rid of latched notes
latched = {}
end
chord_index = chord_index+1
chord[num] = chord_index
if latch and latched[num] then
-- avoid double notes in latch mode
latched[num] = nil
else
changed = true
end
chan = ch
elseif status == 0xb0 and num == 123 and ch == chan then
if debug >= 4 then
print("all notes off")
end
chord = {}
latched = {}
changed = true
end
end
if changed then
-- update the pattern
pattern = {}
function pattern_from_chord(pattern, chord)
for num, val in pairs(chord) do
table.insert(pattern, num)
for i = 1, down do
if num-i*12 >= 0 then
table.insert(pattern, num-i*12)
end
end
for i = 1, up do
if num+i*12 <= 127 then
table.insert(pattern, num+i*12)
end
end
end
end
pattern_from_chord(pattern, chord)
if latch then
-- add any latched notes
pattern_from_chord(pattern, latched)
end
table.sort(pattern) -- order by ascending notes (up pattern)
local n = #pattern
if n > 0 then
if mode == 2 then
-- down pattern, reverse the list
table.sort(pattern, function(a,b) return a > b end)
elseif mode == 3 then
-- add the reversal of the list excluding the last element
for i = 1, n-2 do
table.insert(pattern, pattern[n-i])
end
elseif mode == 4 then
-- add the reversal of the list including the last element
for i = 1, n-1 do
table.insert(pattern, pattern[n-i+1])
end
elseif mode == 5 then
-- order the pattern by chord indices
local k = chord_index+1
local idx = {}
-- build a table of indices which also includes octaves up and
-- down, ordering them first by octave and then by index
function index_from_chord(idx, chord)
for num, val in pairs(chord) do
for i = 1, down do
if num-i*12 >= 0 then
idx[num-i*12] = val - i*k
end
end
idx[num] = val
for i = 1, up do
if num+i*12 <= 127 then
idx[num+i*12] = val + i*k
end
end
end
end
index_from_chord(idx, chord)
if latch then
index_from_chord(idx, latched)
end
table.sort(pattern, function(a,b) return idx[a] < idx[b] end)
elseif mode == 6 then
-- random order
for i = n, 2, -1 do
local j = math.random(i)
pattern[i], pattern[j] = pattern[j], pattern[i]
end
end
if debug >= 2 then
local s = "pattern:"
for i, num in ipairs(pattern) do
s = s .. " " .. num
end
print(s)
end
index = 0 -- reset pattern to the start
else
chord_index = 0 -- pattern is empty, reset the chord index
if debug >= 2 then
print("pattern: <empty>")
end
end
end
if rolling and not bypass then
-- transport is rolling, not bypassed, so the arpeggiator is playing
if last_gate and last_num and
last_gate >= time.sample and last_gate < time.sample_end then
-- Gated notes don't normally fall on a beat, so we detect them
-- here. (If the gate time hasn't been set or we miss it, then the
-- note-off will be taken care of when the next note gets triggered,
-- see below.)
if debug >= 3 then
print("note off", last_num)
end
-- sample-accurate "off" time
local ts = last_gate - time.sample + 1
midiout[k] = { time = ts, data = { 0x80+last_chan, last_num, 100 } }
last_num = nil
k = k+1
end
-- Check whether a beat is due, so that we trigger the next note. We
-- want to do this in a sample-accurate manner in order to avoid jitter,
-- which makes things a little complicated. There are three cases to
-- consider here:
-- (1) Transport just started rolling or the playhead moved for some
-- reason, in which case we *must* output the note immediately in order
-- to not miss a beat (even if we're a bit late).
-- (2) The beat occurs exactly at the beginning of a processing cycle,
-- so we output the note immediately.
-- (3) The beat happens some time during the cycle, in which case we
-- calculate the sample at which the note is due.
local denom = time.ts_denominator * subdiv
-- beat numbers at start and end, scaled by base pulses and subdivisions
local b1, b2 = denom/4*time.beat, denom/4*time.beat_end
-- integral part of these
local bf1, bf2 = math.floor(b1), math.floor(b2)
-- sample times at start and end
local s1, s2 = time.sample, time.sample_end
-- current (nominal, i.e., unscaled) beat number, and its sample time
local bt, ts
if last_beat ~= math.floor(time.beat) or bf1 == b1 then
-- next beat is due immediately
bt, ts = time.beat, time.sample
elseif bf2 > bf1 and bf2 ~= b2 then
-- next beat is due some time in this cycle (we're assuming contant
-- tempo here, hence this number may be off in case the tempo is
-- changing very quickly during the cycle -- so don't do that)
local d = math.ceil((b2-bf2)/(b2-b1)*(s2-s1))
assert(d > 0)
bt, ts = time.beat_end, time.sample_end - d
end
if ts then
-- save the last nominal beat so that we can detect sudden changes of
-- the playhead later (e.g., when transport starts rolling, or at the
-- end of a loop when the playhead wraps around to the beginning)
last_beat = math.floor(bt)
-- get the tempo map information
local tm = Temporal.TempoMap.read ()
local pos = Temporal.timepos_t (ts)
local bbt = tm:bbt_at (pos)
local meter = tm:meter_at (pos)
local tempo = tm:tempo_at (pos)
-- calculate the note-off time in samples, this is used if the gate
-- control is neither 0 nor 1
local gate_ts = ts + math.floor(tm:bbt_duration_at(pos, Temporal.BBT_Offset(0,1,0)):samples() / subdiv * gate)
local n = #pattern
ts = ts - time.sample + 1
if debug >= 1 then
-- print some debugging information: bbt, fractional beat number,
-- sample offset, current meter, current tempo
print (string.format("%s - %g [%d] - %d/%d - %g bpm", bbt:str(),
math.floor(denom*bt)/denom, ts-1,
meter:divisions_per_bar(), meter:note_value(),
tempo:quarter_notes_per_minute()))
end
-- we take a very small gate value (close to 0) to mean legato
-- instead, in which case notes extend to the next unfiltered note
local legato = gate_ts < time.sample_end
function note_off()
if last_num then
-- kill the old note
if debug >= 3 then
print("note off", last_num)
end
midiout[k] = { time = ts, data = { 0x80+last_chan, last_num, 100 } }
last_num = nil
k = k+1
end
end
if not legato then
note_off()
end
if n > 0 then
-- calculate a fractional pulse number from the current bbt
local p = bbt.beats-1 + math.max(0, bbt.ticks) / Temporal.ticks_per_beat
-- Detect meter changes and update the Barlow meter object
-- accordingly.
local mdiv = meter:divisions_per_bar()
if mdiv ~= last_mdiv then
if not barlow_meters[mdiv] then
if debug >= 1 then
print(bt, string.format("%d/%d", mdiv, meter:note_value()))
end
barlow_meters[mdiv] = Meter:new(mdiv)
end
barlow_meter = barlow_meters[mdiv]
last_mdiv = mdiv
end
-- Use the algorithm to determine the pulse weight.
local w, npulses = barlow_meter:pulse (p)
if debug >= 4 then
print(" Beat:", p, " Weight =", w, "/", npulses-1)
end
-- normalize the weight to the 0-1 range
w = w/(npulses-1)
-- filter notes
if w >= minw and w <= maxw then
if legato then
note_off()
end
-- compute the velocity, round to nearest integer
local v = minvel + w * (maxvel-minvel)
v = math.floor(v+0.5)
--print("p", p, "v", v)
-- trigger the new note
if sync then
-- sync pattern to the bbt
local l = #pattern
local k = math.floor(p*subdiv+0.5) -- current index in bar
local n = math.floor(l/npulses) -- bars in pattern
if n > 0 then
k = k + index*npulses
if (k+1) % npulses == 0 then
-- next bar
index = (index+1) % n
end
end
num = pattern[k%l+1]
else
index = index%n + 1
num = pattern[index]
end
if debug >= 3 then
print("note on", num, v)
end
midiout[k] = { time = ts, data = { 0x90+chan, num, v } }
last_num = num
last_chan = chan
if gate < 1 and not legato then
-- Set the sample time at which the note-off is due.
last_gate = gate_ts
else
-- Otherwise don't set the off time in which case the
-- note-off gets triggered automatically above.
last_gate = nil
end
end
end
end
else
-- transport not rolling or bypass; reset the last beat number
last_beat = nil
end
if debug >= 1 and #midiout > 0 then
-- monitor memory usage of the Lua interpreter
print(string.format("mem: %0.2f KB", collectgarbage("count")))
end
end