2140 lines
74 KiB
Lua
2140 lines
74 KiB
Lua
ardour {
|
||
["type"] = "dsp",
|
||
name = "Arpeggiator (Raptor)",
|
||
category = "Effect",
|
||
author = "Albert Gräf",
|
||
license = "GPL",
|
||
description = [[Raptor: The Random Arpeggiator (Raptor 6, Ardour implementation v0.3)
|
||
|
||
Advanced arpeggiator with random note generation, harmonic controls, input pitch and velocity tracking, and automatic modulation of various parameters.
|
||
|
||
In memory of Clarence Barlow (27 December 1945 – 29 June 2023).
|
||
]]
|
||
}
|
||
|
||
-- Raptor Random Arpeggiator for Ardour, ported from the pd-lua version at
|
||
-- https://github.com/agraef/raptor-lua.
|
||
|
||
-- Author: Albert Gräf <aggraef@gmail.com>, Dept. of Music-Informatics,
|
||
-- Johannes Gutenberg University (JGU) of Mainz, Germany, please check
|
||
-- https://agraef.github.io/ for a list of my software.
|
||
|
||
-- Copyright (c) 2021 by Albert Gräf <aggraef@gmail.com>
|
||
|
||
-- Distributed under the GPLv3+, please check the accompanying COPYING file
|
||
-- for details.
|
||
|
||
-- As the Ardour Lua interface wants everything in a single Lua module, this
|
||
-- is a hodgeposge of the modules making up the pd-lua version, with the
|
||
-- Ardour dsp thrown on top that.
|
||
|
||
-- -------------------------------------------------------------------------
|
||
|
||
-- Various helper functions to compute Barlow meters and harmonicities using
|
||
-- the methods from Clarence Barlow's Ratio book (Feedback Papers, Cologne,
|
||
-- 2001)
|
||
|
||
|
||
local M = {}
|
||
|
||
-- list helper functions
|
||
|
||
-- concatenate tables
|
||
function M.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
|
||
|
||
-- reverse a table
|
||
function M.reverse(list)
|
||
local res = {}
|
||
for _, v in ipairs(list) do
|
||
table.insert(res, 1, v)
|
||
end
|
||
return res
|
||
end
|
||
|
||
-- arithmetic sequences
|
||
function M.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
|
||
|
||
-- cycle through a table
|
||
function M.cycle(t, i)
|
||
local n = #t
|
||
if n > 0 then
|
||
while i > n do
|
||
i = i - n
|
||
end
|
||
end
|
||
return t[i]
|
||
end
|
||
|
||
-- some functional programming goodies
|
||
|
||
function M.map(list, fn)
|
||
local res = {}
|
||
for _, v in ipairs(list) do
|
||
table.insert(res, fn(v))
|
||
end
|
||
return res
|
||
end
|
||
|
||
function M.reduce(list, acc, fn)
|
||
for _, v in ipairs(list) do
|
||
acc = fn(acc, v)
|
||
end
|
||
return acc
|
||
end
|
||
|
||
function M.collect(list, acc, fn)
|
||
local res = {acc}
|
||
for _, v in ipairs(list) do
|
||
acc = fn(acc, v)
|
||
table.insert(res, acc)
|
||
end
|
||
return res
|
||
end
|
||
|
||
function M.sum(list)
|
||
return M.reduce(list, 0, function(a,b) return a+b end)
|
||
end
|
||
|
||
function M.prd(list)
|
||
return M.reduce(list, 1, function(a,b) return a*b end)
|
||
end
|
||
|
||
function M.sums(list)
|
||
return M.collect(list, 0, function(a,b) return a+b end)
|
||
end
|
||
|
||
function M.prds(list)
|
||
return M.collect(list, 1, function(a,b) return a*b end)
|
||
end
|
||
|
||
-- Determine the prime factors of an integer. The result is a list with the
|
||
-- prime factors in non-decreasing order.
|
||
|
||
function M.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
|
||
|
||
-- Collect the factors of the integer n and return them as a list of pairs
|
||
-- {p,k} where p are the prime factors in ascending order and k the
|
||
-- corresponding (nonzero) multiplicities. If the given number is a pair {p,
|
||
-- q}, considers p/q as a rational number and returns its prime factors with
|
||
-- positive or negative multiplicities.
|
||
|
||
function M.factors(x)
|
||
if type(x) == "table" then
|
||
local n, m = table.unpack(x)
|
||
local pfs, nfs, mfs = {}, M.factors(n), M.factors(m)
|
||
-- merge the factors in nfs and mfs into a single list
|
||
local i, j, k, N, M = 1, 1, 1, #nfs, #mfs
|
||
while i<=N or j<=M do
|
||
if j>M or (i<=N and mfs[j][1]>nfs[i][1]) then
|
||
pfs[k] = nfs[i]
|
||
k = k+1; i = i+1
|
||
elseif i>N or (j<=M and nfs[i][1]>mfs[j][1]) then
|
||
pfs[k] = mfs[j]
|
||
pfs[k][2] = -mfs[j][2]
|
||
k = k+1; j = j+1
|
||
else
|
||
pfs[k] = nfs[i]
|
||
pfs[k][2] = nfs[i][2] - mfs[j][2]
|
||
k = k+1; i = i+1; j = j+1
|
||
end
|
||
end
|
||
return pfs
|
||
else
|
||
local pfs, pf = {}, M.factor(x)
|
||
if next(pf) then
|
||
local j, n = 1, #pf
|
||
pfs[j] = {pf[1], 1}
|
||
for i = 2, n do
|
||
if pf[i] == pfs[j][1] then
|
||
pfs[j][2] = pfs[j][2] + 1
|
||
else
|
||
j = j+1
|
||
pfs[j] = {pf[i], 1}
|
||
end
|
||
end
|
||
end
|
||
return pfs
|
||
end
|
||
end
|
||
|
||
-- Probability functions. These are used with some of the random generation
|
||
-- functions below.
|
||
|
||
-- Create random permutations. Chooses n random values from a list ms of input
|
||
-- values according to a probability distribution given by a list ws of
|
||
-- weights. NOTES: ms and ws should be of the same size, otherwise excess
|
||
-- elements will be chosen at random. In particular, if ws is empty or missing
|
||
-- then shuffle(n, ms) will simply return n elements chosen from ms at random
|
||
-- using a uniform distribution. ms and ws and are modified *in place*,
|
||
-- removing chosen elements, so that their final contents will be the elements
|
||
-- *not* chosen and their corresponding weight distribution.
|
||
|
||
function M.shuffle(n, ms, ws)
|
||
local res = {}
|
||
if ws == nil then
|
||
-- simply choose elements at random, uniform distribution
|
||
ws = {}
|
||
end
|
||
while next(ms) ~= nil and n>0 do
|
||
-- accumulate weights
|
||
local sws = M.sums(ws)
|
||
local s = sws[#sws]
|
||
table.remove(sws, 1)
|
||
-- pick a random index
|
||
local k, r = 0, math.random()*s
|
||
--print("r = ", r, "sws = ", table.unpack(sws))
|
||
for i = 1, #sws do
|
||
if r < sws[i] then
|
||
k = i; break
|
||
end
|
||
end
|
||
-- k may be out of range if ws and ms aren't of the same size, in which
|
||
-- case we simply pick an element at random
|
||
if k==0 or k>#ms then
|
||
k = math.random(#ms)
|
||
end
|
||
table.insert(res, ms[k])
|
||
n = n-1; table.remove(ms, k);
|
||
if k<=#ws then
|
||
table.remove(ws, k)
|
||
end
|
||
end
|
||
return res
|
||
end
|
||
|
||
-- Calculate modulated values. This is used for all kinds of parameters which
|
||
-- can vary automatically according to pulse strength, such as note
|
||
-- probability, velocity, gate, etc.
|
||
|
||
function M.mod_value(x1, x2, b, w)
|
||
-- x2 is the nominal value which is always output if b==0. As b increases
|
||
-- or decreases, the range extends downwards towards x1. (Normally,
|
||
-- x2>x1, but you can reverse bounds to have the range extend upwards.)
|
||
if b >= 0 then
|
||
-- positive bias: mod_value(w) -> x1 as w->0, -> x2 as w->1
|
||
-- zero bias: mod_value(w) == x2 (const.)
|
||
return x2-b*(1-w)*(x2-x1)
|
||
else
|
||
-- negative bias: mod_value(w) -> x1 as w->1, -> x2 as w->0
|
||
return x2+b*w*(x2-x1)
|
||
end
|
||
end
|
||
|
||
-- Barlow meters. This stuff is mostly a verbatim copy of the guts of
|
||
-- meter.pd_lua, please check that module for details.
|
||
|
||
-- 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.
|
||
|
||
function M.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
|
||
|
||
-- Compute pulse strengths according to Barlow's indispensability formula from
|
||
-- the Ratio book.
|
||
|
||
function M.indisp(q)
|
||
local function ind(q, k)
|
||
-- prime indispensabilities
|
||
local function pind(q, k)
|
||
local function ind1(q, k)
|
||
local i = ind(M.reverse(M.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 = M.prds(q)
|
||
local t = M.reverse(M.prds(M.reverse(q)))
|
||
return
|
||
M.sum(M.map(M.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 = M.factor(q)
|
||
end
|
||
if type(q) ~= "table" then
|
||
error("invalid argument, must be an integer or table of primes")
|
||
else
|
||
return M.map(M.seq(0,M.prd(q)-1), function(k) return ind(q,k) end)
|
||
end
|
||
end
|
||
|
||
-- Barlow harmonicities from the Ratio book. These are mostly ripped out of an
|
||
-- earlier version of the Raptor random arpeggiator programs (first written in
|
||
-- Q, then rewritten in Pure, and now finally ported to Lua).
|
||
|
||
-- Some "standard" 12 tone scales and prime valuation functions to play with.
|
||
-- Add others as needed. We mostly use the just scale and the standard Barlow
|
||
-- valuation here.
|
||
|
||
M.just = -- standard just intonation, a.k.a. the Ptolemaic (or Didymic) scale
|
||
{ {1,1}, {16,15}, {9,8}, {6,5}, {5,4}, {4,3}, {45,32},
|
||
{3,2}, {8,5}, {5,3}, {16,9}, {15,8}, {2,1} }
|
||
M.pyth = -- pythagorean (3-limit) scale
|
||
{ {1,1}, {2187,2048}, {9,8}, {32,27}, {81,64}, {4,3}, {729,512},
|
||
{3,2}, {6561,4096}, {27,16}, {16,9}, {243,128}, {2,1} }
|
||
M.mean4 = -- 1/4 comma meantone scale, Barlow (re-)rationalization
|
||
{ {1,1}, {25,24}, {10,9}, {6,5}, {5,4}, {4,3}, {25,18},
|
||
{3,2}, {25,16}, {5,3}, {16,9}, {15,8}, {2,1} }
|
||
|
||
function M.barlow(p) return 2*(p-1)*(p-1)/p end
|
||
function M.euler(p) return p-1 end
|
||
-- "mod 2" versions (octave is eliminated)
|
||
function M.barlow2(p) if p==2 then return 0 else return M.barlow(p) end end
|
||
function M.euler2(p) if p==2 then return 0 else return M.euler(p) end end
|
||
|
||
-- Harmonicity computation.
|
||
|
||
-- hrm({p,q}, pv) computes the disharmonicity of the interval p/q using the
|
||
-- prime valuation function pv.
|
||
|
||
-- hrm_dist({p1,q1}, {p2,q2}, pv) computes the harmonic distance between two
|
||
-- pitches, i.e., the disharmonicity of the interval between {p1,q1} and
|
||
-- {p2,q2}.
|
||
|
||
-- hrm_scale(S, pv) computes the disharmonicity metric of a scale S, i.e., the
|
||
-- pairwise disharmonicities of all intervals in the scale. The input is a
|
||
-- list of intervals as {p,q} pairs, the output is the distance matrix.
|
||
|
||
function M.hrm(x, pv)
|
||
return M.sum(M.map(M.factors(x),
|
||
function(f) local p, k = table.unpack(f)
|
||
return math.abs(k) * pv(p)
|
||
end))
|
||
end
|
||
|
||
function M.hrm_dist(x, y, pv)
|
||
local p1, q1 = table.unpack(x)
|
||
local p2, q2 = table.unpack(y)
|
||
return M.hrm({p1*q2,p2*q1}, pv)
|
||
end
|
||
|
||
function M.hrm_scale(S, pv)
|
||
return M.map(S,
|
||
function(s)
|
||
return M.map(S, function(t) return M.hrm_dist(s, t, pv) end)
|
||
end)
|
||
end
|
||
|
||
-- Some common tables for convenience and testing. These are all based on a
|
||
-- standard 12-tone just tuning. NOTE: The given reference tables use rounded
|
||
-- values, but are good enough for most practical purposes; you might want to
|
||
-- employ these to avoid the calculation cost.
|
||
|
||
-- Barlow's "indigestibility" harmonicity metric
|
||
-- M.bgrad = {0,13.07,8.33,10.07,8.4,4.67,16.73,3.67,9.4,9.07,9.33,12.07,1}
|
||
M.bgrad = M.map(M.just, function(x) return M.hrm(x, M.barlow) end)
|
||
|
||
-- Euler's "gradus suavitatis" (0-based variant)
|
||
-- M.egrad = {0,10,7,7,6,4,13,3,7,6,8,9,1}
|
||
M.egrad = M.map(M.just, function(x) return M.hrm(x, M.euler) end)
|
||
|
||
-- In an arpeggiator we might want to treat different octaves of the same
|
||
-- pitch as equivalent, in which case we can use the following "mod 2" tables:
|
||
M.bgrad2 = M.map(M.just, function(x) return M.hrm(x, M.barlow2) end)
|
||
M.egrad2 = M.map(M.just, function(x) return M.hrm(x, M.euler2) end)
|
||
|
||
-- But in the following we stick to the standard Barlow table.
|
||
M.grad = M.bgrad
|
||
|
||
-- Calculate the harmonicity of the interval between two (MIDI) notes.
|
||
function M.hm(n, m)
|
||
local d = math.max(n, m) - math.min(n, m)
|
||
return 1/(1+M.grad[d%12+1])
|
||
end
|
||
|
||
-- Use this instead if you also want to keep account of octaves.
|
||
function M.hm2(n, m)
|
||
local d = math.max(n, m) - math.min(n, m)
|
||
return 1/(1+M.grad[d%12+1]+(d//12)*M.grad[13])
|
||
end
|
||
|
||
-- Calculate the average harmonicity (geometric mean) of a MIDI note relative
|
||
-- to a given chord (specified as a list of MIDI notes).
|
||
function M.hv(ns, m)
|
||
if next(ns) ~= nil then
|
||
local xs = M.map(ns, function(n) return M.hm(m, n) end)
|
||
return M.prd(xs)^(1/#xs)
|
||
else
|
||
return 1
|
||
end
|
||
end
|
||
|
||
-- Sort the MIDI notes in ms according to descending average harmonicities
|
||
-- w.r.t. the MIDI notes in ns. This allows you to quickly pick the "best"
|
||
-- (harmonically most pleasing) MIDI notes among given alternatives ms
|
||
-- w.r.t. a given chord ns.
|
||
function M.besthv(ns, ms)
|
||
local mhv = M.map(ms, function(m) return {m, M.hv(ns, m)} end)
|
||
table.sort(mhv, function(x, y) return x[2]>y[2] or
|
||
(x[2]==y[2] and x[1]<y[1]) end)
|
||
return M.map(mhv, function(x) return x[1] end)
|
||
end
|
||
|
||
-- Randomized note filter. This is the author's (in)famous Raptor algorithm.
|
||
-- It needs a whole bunch of parameters, but also delivers much more
|
||
-- interesting results and can produce randomized chords as well. Basically,
|
||
-- it performs a random walk guided by Barlow harmonicities and
|
||
-- indispensabilities. The parameters are:
|
||
|
||
-- ns: input notes (chord memory of the arpeggiator, as in besthv these are
|
||
-- used to calculate the average harmonicities)
|
||
|
||
-- ms: candidate output notes (these will be filtered and participate in the
|
||
-- random walk)
|
||
|
||
-- w: indispensability value used to modulate the various parameters
|
||
|
||
-- nmax, nmod: range and modulation of the density (maximum number of notes
|
||
-- in each step)
|
||
|
||
-- smin, smax, smod: range and modulation of step widths, which limits the
|
||
-- steps between notes in successive pulses
|
||
|
||
-- dir, mode, uniq: arpeggio direction (0 = random, 1 = up, -1 = down), mode
|
||
-- (0 = random, 1 = up, 2 = down, 3 = up-down, 4 = down-up), and whether
|
||
-- repeated notes are disabled (uniq flag)
|
||
|
||
-- hmin, hmax, hmod: range and modulation of eligible harmonicities, which are
|
||
-- used to filter candidate notes based on average harmonicities w.r.t. the
|
||
-- input notes
|
||
|
||
-- pref, prefmod: range and modulation of harmonic preference. This is
|
||
-- actually one of the most important and effective parameters in the Raptor
|
||
-- algorithm which drives the random note selection process. A pref value
|
||
-- between -1 and 1 determines the weighted probabilities used to pick notes
|
||
-- at random. pref>0 gives preference to notes with high harmonicity, pref<0
|
||
-- to notes with low harmonicity, and pref==0 ignores harmonicity (in which
|
||
-- case all eligible notes are chosen with the same probability). The prefs
|
||
-- parameter can also be modulated by pulse strengths as indicated by prefmod
|
||
-- (prefmod>0 lowers preference on weak pulses, prefmod<0 on strong pulses).
|
||
|
||
function M.harm_filter(w, hmin, hmax, hmod, ns, ms)
|
||
-- filters notes according to harmonicities and a given pulse weight w
|
||
if next(ns) == nil then
|
||
-- empty input (no eligible notes)
|
||
return {}
|
||
else
|
||
local res = {}
|
||
for _,m in ipairs(ms) do
|
||
local h = M.hv(ns, m)
|
||
-- modulate: apply a bias determined from hmod and w
|
||
if hmod > 0 then
|
||
h = h^(1-hmod*(1-w))
|
||
elseif hmod < 0 then
|
||
h = h^(1+hmod*w)
|
||
end
|
||
-- check that the (modulated) harmonicity is within prescribed bounds
|
||
if h>=hmin and h<=hmax then
|
||
table.insert(res, m)
|
||
end
|
||
end
|
||
return res
|
||
end
|
||
end
|
||
|
||
function M.step_filter(w, smin, smax, smod, dir, mode, cache, ms)
|
||
-- filters notes according to the step width parameters and pulse weight w,
|
||
-- given which notes are currently playing (the cache)
|
||
if next(ms) == nil or dir == 0 then
|
||
return ms, dir
|
||
end
|
||
local res = {}
|
||
while next(res) == nil do
|
||
if next(cache) ~= nil then
|
||
-- non-empty cache, going any direction
|
||
local lo, hi = cache[1], cache[#cache]
|
||
-- NOTE: smin can be negative, allowing us, say, to actually take a
|
||
-- step *down* while going upwards. But we always enforce that smax
|
||
-- is non-negative in order to avoid deadlock situations where *no*
|
||
-- step is valid anymore, and even restarting the pattern doesn't
|
||
-- help. (At least that's what I think, I don't really recall what
|
||
-- the original rationale behind all this was, but since it's in the
|
||
-- original Raptor code, it must make sense somehow. ;-)
|
||
smax = math.max(0, smax)
|
||
smax = math.floor(M.mod_value(math.abs(smin), smax, smod, w)+0.5)
|
||
local function valid_step_min(m)
|
||
if dir==0 then
|
||
return (m>=lo+smin) or (m<=hi-smin)
|
||
elseif dir>0 then
|
||
return m>=lo+smin
|
||
else
|
||
return m<=hi-smin
|
||
end
|
||
end
|
||
local function valid_step_max(m)
|
||
if dir==0 then
|
||
return (m>=lo-smax) and (m<=hi+smax)
|
||
elseif dir>0 then
|
||
return (m>=lo+math.min(0,smin)) and (m<=hi+smax)
|
||
else
|
||
return (m>=lo-smax) and (m<=hi-math.min(0,smin))
|
||
end
|
||
end
|
||
for _,m in ipairs(ms) do
|
||
if valid_step_min(m) and valid_step_max(m) then
|
||
table.insert(res, m)
|
||
end
|
||
end
|
||
elseif dir == 1 then
|
||
-- empty cache, going up, start at bottom
|
||
local lo = ms[1]
|
||
local max = math.floor(M.mod_value(smin, smax, smod, w)+0.5)
|
||
for _,m in ipairs(ms) do
|
||
if m <= lo+max then
|
||
table.insert(res, m)
|
||
end
|
||
end
|
||
elseif dir == -1 then
|
||
-- empty cache, going down, start at top
|
||
local hi = ms[#ms]
|
||
local max = math.floor(M.mod_value(smin, smax, smod, w)+0.5)
|
||
for _,m in ipairs(ms) do
|
||
if m >= hi-max then
|
||
table.insert(res, m)
|
||
end
|
||
end
|
||
else
|
||
-- empty cache, random direction, all notes are eligible
|
||
return ms, dir
|
||
end
|
||
if next(res) == nil then
|
||
-- we ran out of notes, restart the pattern
|
||
-- print("raptor: no notes to play, restart!")
|
||
cache = {}
|
||
if mode==0 then
|
||
dir = 0
|
||
elseif mode==1 or (mode==3 and dir==0) then
|
||
dir = 1
|
||
elseif mode==2 or (mode==4 and dir==0) then
|
||
dir = -1
|
||
else
|
||
dir = -dir
|
||
end
|
||
end
|
||
end
|
||
return res, dir
|
||
end
|
||
|
||
function M.uniq_filter(uniq, cache, ms)
|
||
-- filters out repeated notes (removing notes already in the cache),
|
||
-- depending on the uniq flag
|
||
if not uniq or next(ms) == nil or next(cache) == nil then
|
||
return ms
|
||
end
|
||
local res = {}
|
||
local i, j, k, N, M = 1, 1, 1, #cache, #ms
|
||
while i<=N or j<=M do
|
||
if j>M then
|
||
-- all elements checked, we're done
|
||
return res
|
||
elseif i>N or ms[j]<cache[i] then
|
||
-- current element not in cache, add it
|
||
res[k] = ms[j]
|
||
k = k+1; j = j+1
|
||
elseif ms[j]>cache[i] then
|
||
-- look at next cache element
|
||
i = i+1
|
||
else
|
||
-- current element in cache, skip it
|
||
i = i+1; j = j+1
|
||
end
|
||
end
|
||
return res
|
||
end
|
||
|
||
function M.pick_notes(w, n, pref, prefmod, ns, ms)
|
||
-- pick n notes from the list ms of eligible notes according to the
|
||
-- given harmonic preference
|
||
local ws = {}
|
||
-- calculate weighted harmonicities based on preference; this gives us the
|
||
-- probability distribution for the note selection step
|
||
local p = M.mod_value(0, pref, prefmod, w)
|
||
if p==0 then
|
||
-- no preference, use uniform distribution
|
||
for i = 1, #ms do
|
||
ws[i] = 1
|
||
end
|
||
else
|
||
for i = 1, #ms do
|
||
-- "Frankly, I don't know where the exponent came from," probably
|
||
-- experimentation. ;-)
|
||
ws[i] = M.hv(ns, ms[i]) ^ (p*10)
|
||
end
|
||
end
|
||
return M.shuffle(n, ms, ws)
|
||
end
|
||
|
||
-- The note generator. This is invoked with the current pulse weight w, the
|
||
-- current cache (notes played in the previous step), the input notes ns, the
|
||
-- candidate output notes ms, and all the other parameters that we need
|
||
-- (density: nmax, nmod; harmonicity: hmin, hmax, hmod; step width: smin,
|
||
-- smax, smod; arpeggiator state: dir, mode, uniq; harmonic preference: pref,
|
||
-- prefmod). It returns a selection of notes chosen at random for the given
|
||
-- parameters, along with the updated direction dir of the arpeggiator.
|
||
|
||
function M.rand_notes(w, nmax, nmod,
|
||
hmin, hmax, hmod,
|
||
smin, smax, smod,
|
||
dir, mode, uniq,
|
||
pref, prefmod,
|
||
cache,
|
||
ns, ms)
|
||
-- uniqueness filter: remove repeated notes
|
||
local res = M.uniq_filter(uniq, cache, ms)
|
||
-- harmonicity filter: select notes based on harmonicity
|
||
res = M.harm_filter(w, hmin, hmax, hmod, ns, res)
|
||
-- step filter: select notes based on step widths and arpeggiator state
|
||
-- (this must be the last filter!)
|
||
res, dir = M.step_filter(w, smin, smax, smod, dir, mode, cache, res)
|
||
-- pick notes
|
||
local n = math.floor(M.mod_value(1, nmax, nmod, w)+0.5)
|
||
res = M.pick_notes(w, n, pref, prefmod, ns, res)
|
||
return res, dir
|
||
end
|
||
|
||
local barlow = M
|
||
|
||
-- -------------------------------------------------------------------------
|
||
|
||
-- quick and dirty replacement for kikito's inspect; we mostly need this for
|
||
-- debugging messages, but also when saving data, so the output doesn't need
|
||
-- to be pretty, but should be humanly readable and conform to Lua syntax
|
||
|
||
local function inspect(x)
|
||
if type(x) == "string" then
|
||
return string.format("%q", x)
|
||
elseif type(x) == "table" then
|
||
local s = ""
|
||
local n = 0
|
||
for k,v in pairs(x) do
|
||
if n > 0 then
|
||
s = s .. ", "
|
||
end
|
||
s = s .. string.format("[%s] = %s", inspect(k), inspect(v))
|
||
n = n+1
|
||
end
|
||
return string.format("{ %s }", s)
|
||
else
|
||
return tostring(x)
|
||
end
|
||
end
|
||
|
||
-- -------------------------------------------------------------------------
|
||
|
||
-- Arpeggiator object. In the Pd external, this takes input from the object's
|
||
-- inlets and returns results on the object's outlets. In the Ardour
|
||
-- implementation, the inlets are just method arguments, and the outlets
|
||
-- become the method's return values (there can be more than one, up to one
|
||
-- for each outlet, which are represented as tuples).
|
||
|
||
-- Also, the Ardour implementation replaces the hold toggle with a latch
|
||
-- control, which can be used in a similar fashion but is much more useful.
|
||
|
||
arpeggio = {}
|
||
arpeggio.__index = arpeggio
|
||
|
||
function arpeggio:new(m) -- constructor
|
||
local x = setmetatable(
|
||
{
|
||
-- some reasonable defaults (see also arpeggio:initialize below)
|
||
debug = 0, idx = 0, chord = {}, pattern = {},
|
||
latch = nil, down = -1, up = 1, mode = 0,
|
||
minvel = 60, maxvel = 120, velmod = 1,
|
||
wmin = 0, wmax = 1,
|
||
pmin = 0.3, pmax = 1, pmod = 0,
|
||
gate = 1, gatemod = 0,
|
||
veltracker = 1, minavg = nil, maxavg = nil,
|
||
gain = 1, g = math.exp(-1/3),
|
||
loopstate = 0, loopsize = 0, loopidx = 0, loop = {}, loopdir = "",
|
||
nmax = 1, nmod = 0,
|
||
hmin = 0, hmax = 1, hmod = 0,
|
||
smin = 1, smax = 7, smod = 0,
|
||
uniq = 1,
|
||
pref = 1, prefmod = 0,
|
||
pitchtracker = 0, pitchlo = 0, pitchhi = 0,
|
||
n = 0
|
||
},
|
||
arpeggio)
|
||
x:initialize(m)
|
||
return x
|
||
end
|
||
|
||
function arpeggio:initialize(m)
|
||
-- debugging (bitmask): 1 = pattern, 2 = input, 4 = output
|
||
self.debug = 0
|
||
-- internal state variables
|
||
self.idx = 0
|
||
self.chord = {}
|
||
self.pattern = {}
|
||
self.latch = nil
|
||
self.down, self.up, self.mode = -1, 1, 0
|
||
self.minvel, self.maxvel, self.velmod = 60, 120, 1
|
||
self.pmin, self.pmax, self.pmod = 0.3, 1, 0
|
||
self.wmin, self.wmax = 0, 1
|
||
self.gate, self.gatemod = 1, 0
|
||
-- velocity tracker
|
||
self.veltracker, self.minavg, self.maxavg = 1, nil, nil
|
||
-- This isn't really a "gain" control any more, it's more like a dry/wet
|
||
-- mix (1 = dry, 0 = wet) between set values (minvel, maxvel) and the
|
||
-- calculated envelope of MIDI input notes (minavg, maxavg).
|
||
self.gain = 1
|
||
-- smoothing filter, time in pulses (3 works for me, YMMV)
|
||
local t = 3
|
||
-- filter coefficient
|
||
self.g = math.exp(-1/t)
|
||
-- looper
|
||
self.loopstate = 0
|
||
self.loopsize = 0
|
||
self.loopidx = 0
|
||
self.loop = {}
|
||
self.loopdir = ""
|
||
-- Raptor params, reasonable defaults
|
||
self.nmax, self.nmod = 1, 0
|
||
self.hmin, self.hmax, self.hmod = 0, 1, 0
|
||
self.smin, self.smax, self.smod = 1, 7, 0
|
||
self.uniq = 1
|
||
self.pref, self.prefmod = 1, 0
|
||
self.pitchtracker = 0
|
||
self.pitchlo, self.pitchhi = 0, 0
|
||
-- Barlow meter
|
||
-- XXXTODO: We only do integer pulses currently, so the subdivisions
|
||
-- parameter self.n is currently disabled. Maybe we can find some good use
|
||
-- for it in the future, e.g., for ratchets?
|
||
self.n = 0
|
||
if m == nil then
|
||
m = {4} -- default meter (common time)
|
||
end
|
||
-- initialize the indispensability tables and reset the beat counter
|
||
self.indisp = {}
|
||
self:prepare_meter(m)
|
||
-- return the initial number of beats
|
||
return self.beats
|
||
end
|
||
|
||
-- Barlow indispensability meter computation, cf. barlow.pd_lua. This takes a
|
||
-- zero-based beat number, optionally with a phase in the fractional part to
|
||
-- indicate a sub-pulse below the beat level. We then compute the closest
|
||
-- matching subdivision and compute the corresponding pulse weight, using the
|
||
-- precomputed indispensability tables. The returned result is a pair w,n
|
||
-- denoting the Barlow indispensability weight of the pulse in the range
|
||
-- 0..n-1, where n denotes the total number of beats (number of beats in the
|
||
-- current meter times the current subdivision).
|
||
|
||
-- list helpers
|
||
local tabcat, reverse, cycle, map, seq = barlow.tableconcat, barlow.reverse, barlow.cycle, barlow.map, barlow.seq
|
||
-- Barlow indispensabilities and friends
|
||
local factor, indisp, subdiv = barlow.factor, barlow.indisp, barlow.subdiv
|
||
-- Barlow harmonicities and friends
|
||
local mod_value, rand_notes = barlow.mod_value, barlow.rand_notes
|
||
|
||
function arpeggio:meter(b)
|
||
if b < 0 then
|
||
error("meter: beat index must be nonnegative")
|
||
return
|
||
end
|
||
local beat, f = math.modf(b)
|
||
-- take the beat index modulo the total number of beats
|
||
beat = beat % self.beats
|
||
if self.n > 0 then
|
||
-- compute the closest subdivision for the given fractional phase
|
||
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
|
||
-- no subdivisions, just return the indispensability and number of beats
|
||
-- as is
|
||
local w = self.indisp[1][beat+1]
|
||
return w, self.beats
|
||
end
|
||
end
|
||
|
||
function arpeggio:numarg(x)
|
||
if type(x) == "table" then
|
||
x = x[1]
|
||
end
|
||
if type(x) == "number" then
|
||
return x
|
||
else
|
||
error("arpeggio: expected number, got " .. tostring(x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:intarg(x)
|
||
if type(x) == "table" then
|
||
x = x[1]
|
||
end
|
||
if type(x) == "number" then
|
||
return math.floor(x)
|
||
else
|
||
error("arpeggio: expected integer, got " .. tostring(x))
|
||
end
|
||
end
|
||
|
||
-- the looper
|
||
|
||
function arpeggio:loop_clear()
|
||
-- reset the looper
|
||
self.loopstate = 0
|
||
self.loopidx = 0
|
||
self.loop = {}
|
||
end
|
||
|
||
function arpeggio:loop_set()
|
||
-- set the loop and start playing it
|
||
local n, m = #self.loop, self.loopsize
|
||
local b, p, q = self.beats, self.loopidx, self.idx
|
||
-- NOTE: Use Ableton-style launch quantization here. We quantize start and
|
||
-- end of the loop, as well as m = the target loop size to whole bars, to
|
||
-- account for rhythmic inaccuracies. Otherwise it's just much too easy to
|
||
-- miss bar boundaries when recording a loop.
|
||
m = math.ceil(m/b)*b -- rounding up
|
||
-- beginning of last complete bar in cyclic buffer
|
||
local k = (p-q-b) % 256
|
||
if n <= 0 or m <= 0 or m > 256 or k >= n then
|
||
-- We haven't recorded enough steps for a bar yet, or the target size is
|
||
-- 0, bail out with an empty loop.
|
||
self.loop = {}
|
||
self.loopidx = 0
|
||
self.loopstate = 1
|
||
if m == 0 then
|
||
print("loop: zero loop size")
|
||
else
|
||
print(string.format("loop: got %d steps, need %d.", p>=n and math.max(0, p-q) or q==0 and n or math.max(0, n-b), b))
|
||
end
|
||
return
|
||
end
|
||
-- At this point we have at least 1 bar, starting at k+1, that we can grab;
|
||
-- try extending the loop until we hit the target size.
|
||
local l = b
|
||
while l < m do
|
||
if k >= b then
|
||
k = k-b
|
||
elseif p >= n or (k-b) % 256 < p then
|
||
-- in this case either the cyclic buffer hasn't been filled yet, or
|
||
-- wrapping around would take us past the buffer pointer, so bail out
|
||
break
|
||
else
|
||
-- wrap around to the end of the buffer
|
||
k = (k-b) % 256
|
||
end
|
||
l = l+b
|
||
end
|
||
-- grab l (at most m) steps
|
||
--print(string.format("loop: recorded %d/%d steps %d-%d", l, m, k+1, k+m))
|
||
print(string.format("loop: recorded %d/%d steps", l, m))
|
||
local loop = {}
|
||
for i = k+1, k+l do
|
||
loop[i-k] = cycle(self.loop, i)
|
||
end
|
||
self.loop = loop
|
||
self.loopidx = q % l
|
||
self.loopstate = 1
|
||
end
|
||
|
||
function arpeggio:loop_add(notes, vel, gate)
|
||
-- we only start recording at the first note
|
||
local have_notes = type(notes) == "number" or
|
||
(notes ~= nil and next(notes) ~= nil)
|
||
if have_notes or next(self.loop) ~= nil then
|
||
self.loop[self.loopidx+1] = {notes, vel, gate}
|
||
-- we always *store* up to 256 steps in a cyclic buffer
|
||
self.loopidx = (self.loopidx+1) % 256
|
||
end
|
||
end
|
||
|
||
function arpeggio:loop_get()
|
||
local res = {{}, 0, 0}
|
||
local p, n = self.loopidx, math.min(#self.loop, self.loopsize)
|
||
if p < n then
|
||
res = self.loop[p+1]
|
||
-- we always *read* exactly n steps in a cyclic buffer
|
||
self.loopidx = (p+1) % n
|
||
if p % self.beats == 0 then
|
||
local a, b = p // self.beats + 1, n // self.beats
|
||
print(string.format("loop: playing bar %d/%d", a, b))
|
||
end
|
||
end
|
||
-- we maybe should return the current loopidx here which is used to give
|
||
-- visual feedback about the loop cycle in the Pd external; not sure how to
|
||
-- do this in Ardour, though
|
||
return res
|
||
end
|
||
|
||
local function fexists(name)
|
||
local f=io.open(name,"r")
|
||
if f~=nil then io.close(f) return true else return false end
|
||
end
|
||
|
||
function arpeggio:loop_file(file, cmd)
|
||
-- default for cmd is 1 (save) if loop is playing, 0 (load) otherwise
|
||
cmd = cmd or self.loopstate
|
||
-- apply the loopdir if any
|
||
local path = self.loopdir .. file
|
||
if cmd == 1 then
|
||
-- save: first create a backup copy if the file already exists
|
||
if fexists(path) then
|
||
local k, bakname = 1
|
||
repeat
|
||
bakname = string.format("%s~%d~", path, k)
|
||
k = k+1
|
||
until not fexists(bakname)
|
||
-- ignore errors, if we can't rename the file, we probably can't
|
||
-- overwrite it either
|
||
os.rename(path, bakname)
|
||
end
|
||
local f, err = io.open(path, "w")
|
||
if type(err) == "string" then
|
||
print(string.format("loop: %s", err))
|
||
return
|
||
end
|
||
-- shorten the table to the current loop size if needed
|
||
local loop, n = {}, math.min(#self.loop, self.loopsize)
|
||
table.move(self.loop, 1, n, 1, loop)
|
||
-- add some pretty-printing
|
||
local function bars(level, count)
|
||
if level == 1 and count%self.beats == 0 then
|
||
return string.format("-- bar %d", count//self.beats+1)
|
||
end
|
||
end
|
||
f:write(string.format("-- saved by Raptor %s\n", os.date()))
|
||
f:write(inspect(loop, {extra = 1, addin = bars}))
|
||
f:close()
|
||
print(string.format("loop: %s: saved %d steps", file, n))
|
||
elseif cmd == 0 then
|
||
-- load: check that file exists and is loadable
|
||
local f, err = io.open(path, "r")
|
||
if type(err) == "string" then
|
||
print(string.format("loop: %s", err))
|
||
return
|
||
end
|
||
local fun, err = load("return " .. f:read("a"))
|
||
f:close()
|
||
if type(err) == "string" or type(fun) ~= "function" then
|
||
print(string.format("loop: %s: invalid format", file))
|
||
else
|
||
local loop = fun()
|
||
if type(loop) ~= "table" then
|
||
print(string.format("loop: %s: invalid format", file))
|
||
else
|
||
self.loop = loop
|
||
self.loopsize = #loop
|
||
self.loopidx = self.idx % math.max(1, self.loopsize)
|
||
self.loopstate = 1
|
||
print(string.format("loop: %s: loaded %d steps", file, #loop))
|
||
return self.loopsize
|
||
end
|
||
end
|
||
elseif cmd == 2 then
|
||
-- check that file exists, report result
|
||
return fexists(path) and 1 or 0
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_loopsize(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.loopsize = math.max(0, math.min(256, x))
|
||
if self.loopstate == 1 then
|
||
-- need to update the loop index in case the loopsize changed
|
||
if self.loopsize > 0 then
|
||
-- also resynchronize the loop with the arpeggiator if needed
|
||
self.loopidx = math.max(self.idx, self.loopidx % self.loopsize)
|
||
else
|
||
self.loopidx = 0
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_loop(x)
|
||
if type(x) == "string" then
|
||
x = {x}
|
||
end
|
||
if type(x) == "table" and type(x[1]) == "string" then
|
||
-- file operations
|
||
self:loop_file(table.unpack(x))
|
||
else
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
if x ~= 0 and self.loopstate == 0 then
|
||
self:loop_set()
|
||
elseif x == 0 and self.loopstate == 1 then
|
||
self:loop_clear()
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_loopdir(x)
|
||
if type(x) == "string" then
|
||
x = {x}
|
||
end
|
||
if type(x) == "table" and type(x[1]) == "string" then
|
||
-- directory for file operations
|
||
self.loopdir = x[1] .. "/"
|
||
end
|
||
end
|
||
|
||
-- velocity tracking
|
||
|
||
function arpeggio:update_veltracker(chord, vel)
|
||
if next(chord) == nil then
|
||
-- reset
|
||
self.minavg, self.maxavg = nil, nil
|
||
if self.debug&2~=0 then
|
||
print(string.format("min = %s, max = %s", self.minavg, self.maxavg))
|
||
end
|
||
elseif vel > 0 then
|
||
-- calculate the velocity envelope
|
||
if not self.minavg then
|
||
self.minavg = self.minvel
|
||
end
|
||
self.minavg = self.minavg*self.g + vel*(1-self.g)
|
||
if not self.maxavg then
|
||
self.maxavg = self.maxvel
|
||
end
|
||
self.maxavg = self.maxavg*self.g + vel*(1-self.g)
|
||
if self.debug&2~=0 then
|
||
print(string.format("vel min = %g, max = %g", self.minavg, self.maxavg))
|
||
end
|
||
end
|
||
end
|
||
|
||
function arpeggio:velrange()
|
||
if self.veltracker ~= 0 then
|
||
local g = self.gain
|
||
local min = self.minavg or self.minvel
|
||
local max = self.maxavg or self.maxvel
|
||
min = g*self.minvel + (1-g)*min
|
||
max = g*self.maxvel + (1-g)*max
|
||
return min, max
|
||
else
|
||
return self.minvel, self.maxvel
|
||
end
|
||
end
|
||
|
||
-- output the next note in the pattern and switch to the next pulse
|
||
-- The result is a tuple notes, vel, gate, w, n, where vel is the velocity,
|
||
-- gate the gate value (normalized duration), w the pulse weight
|
||
-- (indispensability), and n the total number of pulses. The first return
|
||
-- value indicates the notes to play. This may either be a singleton number or
|
||
-- a list (which can also be empty, or contain multiple note numbers).
|
||
function arpeggio:pulse()
|
||
local w, n = self:meter(self.idx)
|
||
-- normalized pulse strength
|
||
local w1 = w/math.max(1,n-1)
|
||
-- corresponding MIDI velocity
|
||
local minvel, maxvel = self:velrange()
|
||
local vel =
|
||
math.floor(mod_value(minvel, maxvel, self.velmod, w1))
|
||
local gate, notes = 0, nil
|
||
if self.loopstate == 1 and self.loopsize > 0 then
|
||
-- notes come straight from the loop, input is ignored
|
||
notes, vel, gate = table.unpack(self:loop_get())
|
||
self.idx = (self.idx + 1) % self.beats
|
||
return notes, vel, gate, w, n
|
||
end
|
||
if type(self.pattern) == "function" then
|
||
notes = self.pattern(w1)
|
||
elseif next(self.pattern) ~= nil then
|
||
notes = cycle(self.pattern, self.idx+1)
|
||
end
|
||
if notes ~= nil then
|
||
-- note filtering
|
||
local ok = true
|
||
local wmin, wmax = self.wmin, self.wmax
|
||
if w1 >= wmin and w1 <= wmax then
|
||
local pmin, pmax = self.pmin, self.pmax
|
||
-- Calculate the filter probablity. We allow for negative pmod values
|
||
-- here, in which case stronger pulses tend to be filtered out first
|
||
-- rather than weaker ones.
|
||
local p = mod_value(pmin, pmax, self.pmod, w1)
|
||
local r = math.random()
|
||
if self.debug&4~=0 then
|
||
print(string.format("w = %g, wmin = %g, wmax = %g, p = %g, r = %g",
|
||
w1, wmin, wmax, p, r))
|
||
end
|
||
ok = r <= p
|
||
else
|
||
ok = false
|
||
end
|
||
if ok then
|
||
-- modulated gate value
|
||
gate = mod_value(0, self.gate, self.gatemod, w1)
|
||
-- output notes (there may be more than one in Raptor mode)
|
||
if self.debug&4~=0 then
|
||
print(string.format("idx = %g, notes = %s, vel = %g, gate = %g", self.idx, inspect(notes), vel, gate))
|
||
end
|
||
else
|
||
notes = {}
|
||
end
|
||
else
|
||
notes = {}
|
||
end
|
||
self:loop_add(notes, vel, gate)
|
||
self.idx = (self.idx + 1) % self.beats
|
||
return notes, vel, gate, w, n
|
||
end
|
||
|
||
-- panic clears the chord memory and pattern
|
||
function arpeggio:panic()
|
||
self.chord = {}
|
||
self.pattern = {}
|
||
self.last_q = nil
|
||
-- XXXFIXME: Catch 22 here. This method gets invoked when transport starts
|
||
-- rolling (at which time Ardour sends a bunch of all-note-offs to all
|
||
-- channels). Unfortunately, the following line would then override the
|
||
-- latch control of the plugin, which we don't want. So we have to disable
|
||
-- the following call for now. This means that even the panic button won't
|
||
-- really get rid of the latched notes, you must turn off the latch control
|
||
-- explicitly to make them go away. (However, the current pattern gets
|
||
-- cleared anyway, so hopefully nobody will ever notice.)
|
||
--self:set_latch(0)
|
||
self:update_veltracker({}, 0)
|
||
end
|
||
|
||
-- change the current pulse index
|
||
function arpeggio:set_idx(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" and self.idx ~= x then
|
||
self.idx = math.max(0, x) % self.beats
|
||
if self.loopstate == 1 then
|
||
self.loopidx = self.idx % math.max(1, math.min(#self.loop, self.loopsize))
|
||
end
|
||
end
|
||
end
|
||
|
||
-- pattern computation
|
||
|
||
local function transp(chord, i)
|
||
return map(chord, function (n) return n+12*i end)
|
||
end
|
||
|
||
function arpeggio:pitchrange(a, b)
|
||
if self.pitchtracker == 0 then
|
||
-- just octave range
|
||
a = math.max(0, math.min(127, a+12*self.down))
|
||
b = math.max(0, math.min(127, b+12*self.up))
|
||
elseif self.pitchtracker == 1 then
|
||
-- full range tracker
|
||
a = math.max(0, math.min(127, a+12*self.down+self.pitchlo))
|
||
b = math.max(0, math.min(127, b+12*self.up+self.pitchhi))
|
||
elseif self.pitchtracker == 2 then
|
||
-- treble tracker
|
||
a = math.max(0, math.min(127, b+12*self.down+self.pitchlo))
|
||
b = math.max(0, math.min(127, b+12*self.up+self.pitchhi))
|
||
elseif self.pitchtracker == 3 then
|
||
-- bass tracker
|
||
a = math.max(0, math.min(127, a+12*self.down+self.pitchlo))
|
||
b = math.max(0, math.min(127, a+12*self.up+self.pitchhi))
|
||
end
|
||
return seq(a, b)
|
||
end
|
||
|
||
function arpeggio:create_pattern(chord)
|
||
-- create a new pattern using the current settings
|
||
local pattern = chord
|
||
-- By default we do outside-in by alternating up-down (i.e., lo-hi), set
|
||
-- this flag to true to get something more Logic-like which goes down-up.
|
||
local logic_like = false
|
||
if next(pattern) == nil then
|
||
-- nothing to see here, move along...
|
||
return pattern
|
||
elseif self.raptor ~= 0 then
|
||
-- Raptor mode: Pick random notes from the eligible range based on
|
||
-- average Barlow harmonicities (cf. barlow.lua). This also combines
|
||
-- with mode 0..5, employing the corresponding Raptor arpeggiation
|
||
-- modes. Note that these patterns may contain notes that we're not
|
||
-- actually playing, if they're harmonically related to the input
|
||
-- chord. Raptor can also play chords rather than just single notes, and
|
||
-- with the right settings you can make it go from plain tonal to more
|
||
-- jazz-like and free to completely atonal, and everything in between.
|
||
local a, b = pattern[1], pattern[#pattern]
|
||
-- NOTE: As this kind of pattern is quite costly to compute, we
|
||
-- implement it as a closure which gets evaluated lazily for each pulse,
|
||
-- rather than precomputing the entire pattern at once as in the
|
||
-- deterministic modes.
|
||
if self.mode == 5 then
|
||
-- Raptor by itself doesn't support mode 5 (outside-in), so we
|
||
-- emulate it by alternating between mode 1 and 2. This isn't quite
|
||
-- the same, but it's as close to outside-in as I can make it. You
|
||
-- might also consider mode 0 (random) as a reasonable alternative
|
||
-- instead.
|
||
local cache, mode, dir
|
||
local function restart()
|
||
-- print("raptor: restart")
|
||
cache = {{}, {}}
|
||
if logic_like then
|
||
mode, dir = 2, -1
|
||
else
|
||
mode, dir = 1, 1
|
||
end
|
||
end
|
||
restart()
|
||
pattern = function(w1)
|
||
local notes, _
|
||
if w1 == 1 then
|
||
-- beginning of bar, restart pattern
|
||
restart()
|
||
end
|
||
notes, _ =
|
||
rand_notes(w1,
|
||
self.nmax, self.nmod,
|
||
self.hmin, self.hmax, self.hmod,
|
||
self.smin, self.smax, self.smod,
|
||
dir, mode, self.uniq ~= 0,
|
||
self.pref, self.prefmod,
|
||
cache[mode],
|
||
chord, self:pitchrange(a, b))
|
||
if next(notes) ~= nil then
|
||
cache[mode] = notes
|
||
end
|
||
if dir>0 then
|
||
mode, dir = 2, -1
|
||
else
|
||
mode, dir = 1, 1
|
||
end
|
||
return notes
|
||
end
|
||
else
|
||
local cache, mode, dir
|
||
local function restart()
|
||
-- print("raptor: restart")
|
||
cache = {}
|
||
mode = self.mode
|
||
dir = 0
|
||
if mode == 1 or mode == 3 then
|
||
dir = 1
|
||
elseif mode == 2 or mode == 4 then
|
||
dir = -1
|
||
end
|
||
end
|
||
restart()
|
||
pattern = function(w1)
|
||
local notes
|
||
if w1 == 1 then
|
||
-- beginning of bar, restart pattern
|
||
restart()
|
||
end
|
||
notes, dir =
|
||
rand_notes(w1,
|
||
self.nmax, self.nmod,
|
||
self.hmin, self.hmax, self.hmod,
|
||
self.smin, self.smax, self.smod,
|
||
dir, mode, self.uniq ~= 0,
|
||
self.pref, self.prefmod,
|
||
cache,
|
||
chord, self:pitchrange(a, b))
|
||
if next(notes) ~= nil then
|
||
cache = notes
|
||
end
|
||
return notes
|
||
end
|
||
end
|
||
else
|
||
-- apply the octave range (not used in raptor mode)
|
||
pattern = {}
|
||
for i = self.down, self.up do
|
||
pattern = tabcat(pattern, transp(chord, i))
|
||
end
|
||
if self.mode == 0 then
|
||
-- random: this is just the run-of-the-mill random pattern permutation
|
||
local n, pat = #pattern, {}
|
||
local p = seq(1, n)
|
||
for i = 1, n do
|
||
local j = math.random(i, n)
|
||
p[i], p[j] = p[j], p[i]
|
||
end
|
||
for i = 1, n do
|
||
pat[i] = pattern[p[i]]
|
||
end
|
||
pattern = pat
|
||
elseif self.mode == 1 then
|
||
-- up (no-op)
|
||
elseif self.mode == 2 then
|
||
-- down
|
||
pattern = reverse(pattern)
|
||
elseif self.mode == 3 then
|
||
-- up-down
|
||
local r = reverse(pattern)
|
||
-- get rid of the repeated note in the middle
|
||
table.remove(pattern)
|
||
pattern = tabcat(pattern, r)
|
||
elseif self.mode == 4 then
|
||
-- down-up
|
||
local r = reverse(pattern)
|
||
table.remove(r)
|
||
pattern = tabcat(reverse(pattern), pattern)
|
||
elseif self.mode == 5 then
|
||
-- outside-in
|
||
local n, pat = #pattern, {}
|
||
local p, q = n//2, n%2
|
||
if logic_like then
|
||
for i = 1, p do
|
||
-- highest note first (a la Logic?)
|
||
pat[2*i-1] = pattern[n+1-i]
|
||
pat[2*i] = pattern[i]
|
||
end
|
||
else
|
||
for i = 1, p do
|
||
-- lowest note first (sounds better IMHO)
|
||
pat[2*i-1] = pattern[i]
|
||
pat[2*i] = pattern[n+1-i]
|
||
end
|
||
end
|
||
if q > 0 then
|
||
pat[n] = pattern[p+1]
|
||
end
|
||
pattern = pat
|
||
end
|
||
end
|
||
if self.debug&1~=0 then
|
||
print(string.format("chord = %s", inspect(chord)))
|
||
print(string.format("pattern = %s", inspect(pattern)))
|
||
end
|
||
return pattern
|
||
end
|
||
|
||
-- latch: keep chord notes when released until new chord or reset
|
||
function arpeggio:set_latch(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
if x ~= 0 then
|
||
self.latch = {table.unpack(self.chord)}
|
||
elseif self.latch then
|
||
self.latch = nil
|
||
self.pattern = self:create_pattern(self.chord)
|
||
end
|
||
end
|
||
end
|
||
|
||
function arpeggio:get_chord()
|
||
return self.latch and self.latch or self.chord
|
||
end
|
||
|
||
-- change the range of the pattern
|
||
function arpeggio:set_up(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.up = math.max(-2, math.min(2, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_down(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.down = math.max(-2, math.min(2, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pitchtracker(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.pitchtracker = math.max(0, math.min(3, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pitchlo(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.pitchlo = math.max(-36, math.min(36, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pitchhi(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.pitchhi = math.max(-36, math.min(36, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
-- change the mode (up, down, etc.)
|
||
function arpeggio:set_mode(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.mode = math.max(0, math.min(5, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
-- this enables Raptor mode with randomized note output
|
||
function arpeggio:set_raptor(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.raptor = math.max(0, math.min(1, x))
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
end
|
||
end
|
||
|
||
-- change min/max velocities, gate, and note probabilities
|
||
function arpeggio:set_minvel(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.minvel = math.max(0, math.min(127, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_maxvel(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.maxvel = math.max(0, math.min(127, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_velmod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.velmod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_veltracker(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.veltracker = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_gain(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.gain = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_gate(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.gate = math.max(0, math.min(10, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_gatemod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.gatemod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pmin(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.pmin = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pmax(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.pmax = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pmod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.pmod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_wmin(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.wmin = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_wmax(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.wmax = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
-- change the raptor parameters (harmonicity, etc.)
|
||
function arpeggio:set_nmax(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.nmax = math.max(0, math.min(10, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_nmod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.nmod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_hmin(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.hmin = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_hmax(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.hmax = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_hmod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.hmod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_smin(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.smin = math.max(-127, math.min(127, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_smax(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.smax = math.max(-127, math.min(127, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_smod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.smod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_uniq(x)
|
||
x = self:intarg(x)
|
||
if type(x) == "number" then
|
||
self.uniq = math.max(0, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_pref(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.pref = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
function arpeggio:set_prefmod(x)
|
||
x = self:numarg(x)
|
||
if type(x) == "number" then
|
||
self.prefmod = math.max(-1, math.min(1, x))
|
||
end
|
||
end
|
||
|
||
local function update_chord(chord, note, vel)
|
||
-- update the chord memory, keeping the notes in ascending order
|
||
local n = #chord
|
||
if n == 0 then
|
||
if vel > 0 then
|
||
table.insert(chord, 1, note)
|
||
end
|
||
return chord
|
||
end
|
||
for i = 1, n do
|
||
if chord[i] == note then
|
||
if vel <= 0 then
|
||
-- note off: remove note
|
||
if i < n then
|
||
table.move(chord, i+1, n, i)
|
||
end
|
||
table.remove(chord)
|
||
end
|
||
return chord
|
||
elseif chord[i] > note then
|
||
if vel > 0 then
|
||
-- insert note
|
||
table.insert(chord, i, note)
|
||
end
|
||
return chord
|
||
end
|
||
end
|
||
-- if we come here, no note has been inserted or deleted yet
|
||
if vel > 0 then
|
||
-- note is larger than all present notes in chord, so it needs to be
|
||
-- inserted at the end
|
||
table.insert(chord, note)
|
||
end
|
||
return chord
|
||
end
|
||
|
||
-- note input; update the internal chord memory and recompute the pattern
|
||
function arpeggio:note(note, vel)
|
||
if self.debug&2~=0 then
|
||
print(string.format("note = %s", inspect({ note, vel })))
|
||
end
|
||
if type(note) == "number" and type(vel) == "number" then
|
||
if self.latch and next(self.chord) == nil and vel>0 then
|
||
-- start new pattern
|
||
self.latch = {}
|
||
end
|
||
update_chord(self.chord, note, vel)
|
||
if self.latch and vel>0 then
|
||
update_chord(self.latch, note, vel)
|
||
end
|
||
self.pattern = self:create_pattern(self:get_chord())
|
||
self:update_veltracker(self:get_chord(), vel)
|
||
end
|
||
end
|
||
|
||
-- this recomputes all indispensability tables
|
||
function arpeggio:prepare_meter(meter)
|
||
local n = 1
|
||
local m = {}
|
||
if type(meter) ~= "table" then
|
||
-- assume singleton number
|
||
meter = { meter }
|
||
end
|
||
for _,q in ipairs(meter) do
|
||
if q ~= math.floor(q) then
|
||
error("arpeggio: meter levels must be integer")
|
||
return
|
||
elseif q < 1 then
|
||
error("arpeggio: meter levels must be positive")
|
||
return
|
||
end
|
||
-- factorize each level as Barlow's formula assumes primes
|
||
m = tabcat(m, factor(q))
|
||
n = n*q
|
||
end
|
||
self.beats = n
|
||
self.last_q = nil
|
||
if n > 1 then
|
||
self.indisp[1] = indisp(m)
|
||
for q = 2, self.n do
|
||
local qs = tabcat(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
|
||
|
||
-- set a new meter (given either as a singleton number or as a list of
|
||
-- numbers) and return the number of pulses
|
||
function arpeggio:set_meter(meter)
|
||
self:prepare_meter(meter)
|
||
return self.beats
|
||
end
|
||
|
||
-- -------------------------------------------------------------------------
|
||
|
||
-- Ardour interface (this is mostly like barlow_arp)
|
||
|
||
-- debug level: This only affects the plugin code. 1: print the current beat
|
||
-- and other important state information, 3: also print note input, 4: print
|
||
-- everything, including note output. Output goes to Ardour's log window.
|
||
-- NOTE: To debug the internal state of the arpeggiator object, including
|
||
-- pattern changes and note generation, use the arp.debug setting below.
|
||
local debug = 0
|
||
|
||
function dsp_ioconfig ()
|
||
return { { midi_in = 1, midi_out = 1, audio_in = -1, audio_out = -1}, }
|
||
end
|
||
|
||
function dsp_options ()
|
||
-- NOTE: We need regular_block_length = true in this plugin to get rid of
|
||
-- some intricate timing issues with scheduled note-offs for gated notes
|
||
-- right at the end of a loop. This sometimes causes hanging notes with
|
||
-- automation when transport wraps around to the loop start. It's unclear
|
||
-- whether the issue is in Ardour (caused by split cycles with automation)
|
||
-- or some unkown bug in the plugin. But the option makes it go away (which
|
||
-- seems to indicate that the issue is on the Ardour side).
|
||
return { time_info = true, regular_block_length = true }
|
||
end
|
||
|
||
local hrm_scalepoints = { ["0.09 (minor 7th and 3rd)"] = 0.09, ["0.1 (major 2nd and 3rd)"] = 0.1, ["0.17 (4th)"] = 0.17, ["0.21 (5th)"] = 0.21, ["1 (unison, octave)"] = 1 }
|
||
|
||
local params = {
|
||
{ type = "input", name = "bypass", min = 0, max = 1, default = 0, toggled = true, doc = "bypass the arpeggiator, pass through input notes" },
|
||
{ type = "input", name = "division", min = 1, max = 7, default = 1, integer = true, doc = "number of subdivisions of the beat" },
|
||
{ type = "input", name = "pgm", min = 0, max = 128, default = 0, integer = true, doc = "program change", scalepoints = { default = 0 } },
|
||
{ type = "input", name = "latch", min = 0, max = 1, default = 0, toggled = true, doc = "toggle latch mode" },
|
||
{ type = "input", name = "up", min = -2, max = 2, default = 1, integer = true, doc = "octave range up" },
|
||
{ type = "input", name = "down", min = -2, max = 2, default = -1, integer = true, doc = "octave range down" },
|
||
-- Raptor's usual default for the pattern is 0 = random, but 1 = up
|
||
-- seems to be a more sensible choice.
|
||
{ type = "input", name = "mode", min = 0, max = 5, default = 1, enum = true, doc = "pattern style",
|
||
scalepoints =
|
||
{ ["0 random"] = 0, ["1 up"] = 1, ["2 down"] = 2, ["3 up-down"] = 3, ["4 down-up"] = 4, ["5 outside-in"] = 5 } },
|
||
{ type = "input", name = "raptor", min = 0, max = 1, default = 0, toggled = true, doc = "toggle raptor mode" },
|
||
{ type = "input", name = "minvel", min = 0, max = 127, default = 60, integer = true, doc = "minimum velocity" },
|
||
{ type = "input", name = "maxvel", min = 0, max = 127, default = 120, integer = true, doc = "maximum velocity" },
|
||
{ type = "input", name = "velmod", min = -1, max = 1, default = 1, doc = "automatic velocity modulation according to current pulse strength" },
|
||
{ type = "input", name = "gain", min = 0, max = 1, default = 1, doc = "wet/dry mix between input velocity and set values (min/max velocity)" },
|
||
-- Pd Raptor allows this to go from 0 to 1000%, but we only support
|
||
-- 0-100% here
|
||
{ type = "input", name = "gate", min = 0, max = 1, default = 1, doc = "gate as fraction of pulse length", scalepoints = { legato = 0 } },
|
||
{ type = "input", name = "gatemod", min = -1, max = 1, default = 0, doc = "automatic gate modulation according to current pulse strength" },
|
||
{ type = "input", name = "wmin", min = 0, max = 1, default = 0, doc = "minimum note weight" },
|
||
{ type = "input", name = "wmax", min = 0, max = 1, default = 1, doc = "maximum note weight" },
|
||
{ type = "input", name = "pmin", min = 0, max = 1, default = 0.3, doc = "minimum note probability" },
|
||
{ type = "input", name = "pmax", min = 0, max = 1, default = 1, doc = "maximum note probability" },
|
||
{ type = "input", name = "pmod", min = -1, max = 1, default = 0, doc = "automatic note probability modulation according to current pulse strength" },
|
||
{ type = "input", name = "hmin", min = 0, max = 1, default = 0, doc = "minimum harmonicity", scalepoints = hrm_scalepoints },
|
||
{ type = "input", name = "hmax", min = 0, max = 1, default = 1, doc = "maximum harmonicity", scalepoints = hrm_scalepoints },
|
||
{ type = "input", name = "hmod", min = -1, max = 1, default = 0, doc = "automatic harmonicity modulation according to current pulse strength" },
|
||
{ type = "input", name = "pref", min = -1, max = 1, default = 1, doc = "harmonic preference" },
|
||
{ type = "input", name = "prefmod", min = -1, max = 1, default = 0, doc = "automatic harmonic preference modulation according to current pulse strength" },
|
||
{ type = "input", name = "smin", min = -12, max = 12, default = 1, integer = true, doc = "minimum step size" },
|
||
{ type = "input", name = "smax", min = -12, max = 12, default = 7, integer = true, doc = "maximum step size" },
|
||
{ type = "input", name = "smod", min = -1, max = 1, default = 0, doc = "automatic step size modulation according to current pulse strength" },
|
||
{ type = "input", name = "nmax", min = 0, max = 10, default = 1, integer = true, doc = "maximum polyphony (number of simultaneous notes)" },
|
||
{ type = "input", name = "nmod", min = -1, max = 1, default = 0, doc = "automatic modulation of the number of notes according to current pulse strength" },
|
||
{ type = "input", name = "uniq", min = 0, max = 1, default = 1, toggled = true, doc = "don't repeat notes in consecutive steps" },
|
||
{ type = "input", name = "pitchhi", min = -36, max = 36, default = 0, integer = true, doc = "extended pitch range up in semitones (raptor mode)" },
|
||
{ type = "input", name = "pitchlo", min = -36, max = 36, default = 0, integer = true, doc = "extended pitch range down in semitones (raptor mode)" },
|
||
{ type = "input", name = "pitchtracker", min = 0, max = 3, default = 0, enum = true, doc = "pitch tracker mode, follow input to adjust the pitch range (raptor mode)",
|
||
scalepoints =
|
||
{ ["0 off"] = 0, ["1 on"] = 1, ["2 treble"] = 2, ["3 bass"] = 3 } },
|
||
{ type = "input", name = "inchan", min = 0, max = 16, default = 0, integer = true, doc = "input channel (0 = omni = all channels)", scalepoints = { omni = 0 } },
|
||
{ type = "input", name = "outchan", min = 0, max = 16, default = 0, integer = true, doc = "input channel (0 = omni = input channel)", scalepoints = { omni = 0 } },
|
||
{ type = "input", name = "loopsize", min = 0, max = 16, default = 4, integer = true, doc = "loop size (number of bars)" },
|
||
{ type = "input", name = "loop", min = 0, max = 1, default = 0, toggled = true, doc = "toggle loop mode" },
|
||
{ type = "input", name = "mute", min = 0, max = 1, default = 0, toggled = true, doc = "turn the arpeggiator off, suppress all note output" },
|
||
}
|
||
|
||
local n_params = #params
|
||
local int_param = map(params, function(x) return x.integer == true or x.enum == true or x.toggled == true end)
|
||
|
||
function dsp_params ()
|
||
return params
|
||
end
|
||
|
||
-- This is basically a collection of presets from the Pd external, with some
|
||
-- (very) minor adjustments / bugfixes where I saw fit. The program numbers
|
||
-- assume a GM patch set, if your synth isn't GM-compatible then you'll have
|
||
-- to adjust them accordingly. NOTE: The tr808 preset assumes a GM-compatible
|
||
-- drumkit, so it outputs through MIDI channel 10 by default; other presets
|
||
-- leave the output channel as is.
|
||
|
||
local raptor_presets = {
|
||
{ name = "default", params = { bypass = 0, latch = 0, division = 1, pgm = 0, up = 1, down = -1, mode = 1, raptor = 0, minvel = 60, maxvel = 120, velmod = 1, gain = 1, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.3, pmax = 1, pmod = 0, hmin = 0, hmax = 1, hmod = 0, pref = 1, prefmod = 0, smin = 1, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = 0, pitchtracker = 0, inchan = 0, outchan = 0, loopsize = 4, loop = 0, mute = 0 } },
|
||
{ name = "arp", params = { pgm = 26, up = 0, down = -1, mode = 3, raptor = 1, minvel = 105, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.9, pmax = 1, pmod = -1, hmin = 0.11, hmax = 1, hmod = 0, pref = 0.8, prefmod = 0, smin = 2, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = -12, pitchtracker = 2, loopsize = 4 } },
|
||
{ name = "bass", params = { pgm = 35, up = 0, down = -1, mode = 3, raptor = 1, minvel = 40, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.2, pmax = 1, pmod = 1, hmin = 0.12, hmax = 1, hmod = 0.1, pref = 0.8, prefmod = 0.1, smin = 2, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 7, pitchlo = 0, pitchtracker = 3, loopsize = 4 } },
|
||
{ name = "piano", params = { pgm = 1, up = 1, down = -1, mode = 0, raptor = 1, minvel = 90, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.4, pmax = 1, pmod = 1, hmin = 0.14, hmax = 1, hmod = 0.1, pref = 0.6, prefmod = 0.1, smin = 2, smax = 5, smod = 0, nmax = 2, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = -18, pitchtracker = 2, loopsize = 4 } },
|
||
{ name = "raptor", params = { pgm = 5, up = 1, down = -2, mode = 0, raptor = 1, minvel = 60, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.4, pmax = 0.9, pmod = 0, hmin = 0.09, hmax = 1, hmod = -1, pref = 1, prefmod = 1, smin = 1, smax = 7, smod = 0, nmax = 3, nmod = -1, uniq = 0, pitchhi = 0, pitchlo = 0, pitchtracker = 0, loopsize = 4 } },
|
||
-- some variations of the raptor preset for different instruments
|
||
{ name = "raptor-arp", params = { pgm = 26, up = 0, down = -1, mode = 3, raptor = 1, minvel = 105, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.4, pmax = 0.9, pmod = 0, hmin = 0.09, hmax = 1, hmod = -1, pref = 1, prefmod = 1, smin = 2, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = -12, pitchtracker = 2, loopsize = 4 } },
|
||
{ name = "raptor-bass", params = { pgm = 35, up = 0, down = -1, mode = 3, raptor = 1, minvel = 40, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.4, pmax = 0.9, pmod = 0, hmin = 0.09, hmax = 1, hmod = -1, pref = 1, prefmod = -0.6, smin = 2, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 7, pitchlo = -6, pitchtracker = 3, loopsize = 4 } },
|
||
{ name = "raptor-piano", params = { pgm = 1, up = 1, down = -1, mode = 0, raptor = 1, minvel = 90, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.4, pmax = 0.9, pmod = 0, hmin = 0.09, hmax = 1, hmod = -1, pref = -0.4, prefmod = -0.6, smin = 2, smax = 5, smod = 0, nmax = 2, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = -18, pitchtracker = 2, loopsize = 4 } },
|
||
{ name = "raptor-solo", params = { pgm = 25, up = 0, down = -1, mode = 3, raptor = 1, minvel = 40, maxvel = 110, velmod = 0.5, gain = 0.5, gate = 1, gatemod = 0.5, wmin = 0, wmax = 1, pmin = 0.2, pmax = 0.9, pmod = 0.5, hmin = 0.09, hmax = 1, hmod = -1, pref = -0.4, prefmod = 0, smin = 1, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = 0, pitchtracker = 0, loopsize = 4 } },
|
||
{ name = "tr808", params = { pgm = 26, outchan = 10, up = 0, down = 0, mode = 1, raptor = 0, minvel = 60, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.3, pmax = 1, pmod = 0, hmin = 0, hmax = 1, hmod = 0, pref = 1, prefmod = 0, smin = 1, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = 0, pitchtracker = 0, loopsize = 4 } },
|
||
{ name = "vibes", params = { pgm = 12, up = 0, down = -1, mode = 3, raptor = 1, minvel = 84, maxvel = 120, velmod = 1, gain = 0.5, gate = 1, gatemod = 0, wmin = 0, wmax = 1, pmin = 0.9, pmax = 1, pmod = -1, hmin = 0.14, hmax = 1, hmod = 0.1, pref = 0.6, prefmod = 0.1, smin = 2, smax = 5, smod = 0, nmax = 2, nmod = 0, uniq = 1, pitchhi = -5, pitchlo = -16, pitchtracker = 2, loopsize = 4 } },
|
||
{ name = "weirdmod", params = { pgm = 25, up = 0, down = -1, mode = 5, raptor = 0, minvel = 40, maxvel = 110, velmod = 0.5, gain = 0.5, gate = 1, gatemod = 0.5, wmin = 0, wmax = 1, pmin = 0.2, pmax = 0.9, pmod = 0.5, hmin = 0, hmax = 1, hmod = 0, pref = 1, prefmod = 0, smin = 1, smax = 7, smod = 0, nmax = 1, nmod = 0, uniq = 1, pitchhi = 0, pitchlo = 0, pitchtracker = 0, loopsize = 4 } },
|
||
}
|
||
|
||
function presets()
|
||
return raptor_presets
|
||
end
|
||
|
||
-- pertinent state information, to detect changes
|
||
local last_rolling -- last transport status, to detect changes
|
||
local last_beat -- last beat number
|
||
local last_p -- last pulse index from bbt
|
||
local last_bypass -- last bypass toggle
|
||
local last_mute -- last mute toggle
|
||
-- previous param values, to detect changes
|
||
local last_param = {}
|
||
|
||
-- pertinent note information, to handle note input and output
|
||
local chan = 0 -- MIDI (input and) output channel
|
||
local last_notes -- last notes played
|
||
local last_chan -- MIDI channel of the last notes
|
||
local off_gate -- off time of last notes (sample time)
|
||
local inchan, outchan, pgm = 0, 0, 0
|
||
|
||
-- create the arpeggiator (default meter)
|
||
local last_m = 4 -- last division, to detect changes
|
||
local arp = arpeggio:new(4)
|
||
|
||
-- Debugging output from the arpeggiator object (bitmask):
|
||
-- 1 = pattern, 2 = input, 4 = output (e.g., 7 means "all")
|
||
-- This is intended for debugging purposes only. it spits out *a lot* of
|
||
-- cryptic debug messages in the log window, so it's better to keep this
|
||
-- disabled in production code.
|
||
--arp.debug = 7
|
||
|
||
-- param setters
|
||
|
||
local function arp_set_loopsize(self, x)
|
||
-- need to translate beat numbers to steps
|
||
self:set_loopsize(x*arp.beats)
|
||
end
|
||
|
||
local param_set = { nil, nil, function (_, x) pgm = x end, arp.set_latch, arp.set_up, arp.set_down, arp.set_mode, arp.set_raptor, arp.set_minvel, arp.set_maxvel, arp.set_velmod, arp.set_gain, arp.set_gate, arp.set_gatemod, arp.set_wmin, arp.set_wmax, arp.set_pmin, arp.set_pmax, arp.set_pmod, arp.set_hmin, arp.set_hmax, arp.set_hmod, arp.set_pref, arp.set_prefmod, arp.set_smin, arp.set_smax, arp.set_smod, arp.set_nmax, arp.set_nmod, arp.set_uniq, arp.set_pitchhi, arp.set_pitchlo, arp.set_pitchtracker, function (_, x) inchan = x end, function (_, x) outchan = x end, arp_set_loopsize, arp.set_loop, nil }
|
||
|
||
local function get_chan(ch)
|
||
if outchan == 0 and inchan > 0 then
|
||
ch = inchan-1 -- outchan == inchan > 0 override
|
||
elseif outchan > 0 then
|
||
ch = outchan-1 -- outchan > 0 override
|
||
end
|
||
return ch
|
||
end
|
||
|
||
local function check_chan(ch)
|
||
return inchan == 0 or ch == inchan-1
|
||
end
|
||
|
||
function dsp_run (_, _, n_samples)
|
||
assert (type(midiout) == "table")
|
||
assert (type(time) == "table")
|
||
assert (type(midiout) == "table")
|
||
|
||
local ctrl = CtrlPorts:array ()
|
||
local subdiv = math.floor(ctrl[2])
|
||
local loopsize = math.floor(ctrl[n_params-2])
|
||
-- bypass toggle
|
||
local bypass = ctrl[1] > 0
|
||
-- mute toggle
|
||
local mute = ctrl[n_params] > 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 ()
|
||
|
||
-- detect param changes (subdiv is caught as a meter change below)
|
||
local last_pgm = pgm
|
||
local last_inchan = inchan
|
||
for i = 1, n_params do
|
||
v = ctrl[i]
|
||
if int_param[i] then
|
||
-- Force integer values. (The GUI enforces this, but fractional
|
||
-- values might occur through automation.)
|
||
v = math.floor(v)
|
||
end
|
||
if param_set[i] and v ~= last_param[i] then
|
||
last_param[i] = v
|
||
param_set[i](arp, v)
|
||
end
|
||
end
|
||
|
||
local all_notes_off = false
|
||
if bypass ~= last_bypass then
|
||
last_bypass = bypass
|
||
all_notes_off = true
|
||
end
|
||
|
||
if mute ~= last_mute then
|
||
last_mute = mute
|
||
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
|
||
|
||
if inchan ~= last_inchan and inchan > 0 then
|
||
-- input channel has changed, kill off chord memory
|
||
arp:panic()
|
||
all_notes_off = true
|
||
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
|
||
|
||
if pgm ~= last_pgm or get_chan(chan) ~= chan then
|
||
-- program or output channel has changed, send the program change
|
||
chan = get_chan(chan)
|
||
if pgm > 0 then
|
||
midiout[k] = { time = 1, data = { 0xc0+chan, pgm-1 } }
|
||
k = k+1
|
||
end
|
||
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
|
||
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
|
||
if status == 0xb0 and (num == 123 or num == 64) then
|
||
-- Better to skip these CCs (generated by Ardour to prevent
|
||
-- hanging notes when relocating the playback position, e.g.,
|
||
-- during loop playback). This avoids notes being cut short
|
||
-- further down in the signal path. Also, we don't want those
|
||
-- messages to proliferate if our MIDI gets sent off to another
|
||
-- track. Unfortunately, there's no way to check whether these
|
||
-- events are synthetic or user input. So it seems best to just
|
||
-- ignore them.
|
||
else
|
||
midiout[k] = ev
|
||
k = k+1
|
||
end
|
||
end
|
||
if status == 0x80 or status == 0x90 and val == 0 then
|
||
if check_chan(ch) then
|
||
if debug >= 4 then
|
||
print("note off", num, val)
|
||
end
|
||
arp:note(num, 0)
|
||
end
|
||
elseif status == 0x90 then
|
||
if check_chan(ch) then
|
||
if debug >= 4 then
|
||
print("note on", num, val, "ch", ch)
|
||
end
|
||
arp:note(num, val)
|
||
chan = get_chan(ch)
|
||
end
|
||
elseif not rolling and status == 0xb0 and num == 123 and ch == chan then
|
||
-- This disrupts the arpeggiator during playback, so we only process
|
||
-- these messages (generated by Ardour to prevent hanging notes when
|
||
-- relocating the playback position) if transport is stopped.
|
||
if debug >= 4 then
|
||
print("all notes off")
|
||
end
|
||
arp:panic()
|
||
end
|
||
end
|
||
|
||
if rolling and not bypass and not mute then
|
||
-- transport is rolling, not bypassed, so the arpeggiator is playing
|
||
local function notes_off(ts)
|
||
if last_notes then
|
||
-- kill the old notes
|
||
for _, num in ipairs(last_notes) do
|
||
if debug >= 3 then
|
||
print("note off", num)
|
||
end
|
||
midiout[k] = { time = ts, data = { 0x80+last_chan, num, 100 } }
|
||
k = k+1
|
||
end
|
||
last_notes = nil
|
||
end
|
||
end
|
||
if off_gate and last_notes and
|
||
off_gate >= time.sample and off_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-offs will be taken care of when the next notes get triggered,
|
||
-- see below.)
|
||
-- sample-accurate "off" time
|
||
local ts = off_gate - time.sample + 1
|
||
notes_off(ts)
|
||
end
|
||
-- Check whether a beat is due, so that we trigger the next notes. We
|
||
-- want to do this in a sample-accurate manner in order to avoid jitter,
|
||
-- check barlow_arp.lua for details.
|
||
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)
|
||
-- current meter (divisions per bar * subdivisions)
|
||
local m = meter:divisions_per_bar() * subdiv
|
||
-- detect meter changes
|
||
if m ~= last_m then
|
||
last_m = m
|
||
arp:set_meter(m)
|
||
-- we also need to update the loop size here
|
||
arp_set_loopsize(arp, loopsize)
|
||
end
|
||
-- calculate a fractional pulse number from the current bbt
|
||
local p = bbt.beats-1 + math.max(0, bbt.ticks) / Temporal.ticks_per_beat
|
||
-- round to current pulse index
|
||
p = math.floor(p * subdiv)
|
||
if p == last_p then
|
||
-- Avoid triggering the same pulse twice (probably a timing issue
|
||
-- which seems to happen when the playback position is relocated,
|
||
-- e.g., at the beginning of a loop).
|
||
goto skip
|
||
end
|
||
last_p = p
|
||
-- grab some notes from the arpeggiator
|
||
arp:set_idx(p) -- in case we've changed position
|
||
local notes, vel, gate, w, n = arp:pulse()
|
||
-- Make sure that the gate is clamped to the 0-1 range, since we
|
||
-- don't support overlapping notes in the current implementation.
|
||
gate = math.max(0, math.min(1, gate))
|
||
--print(string.format("[%d] notes", p), inspect(notes), vel, gate, w, n)
|
||
-- the arpeggiator may return a singleton note, make sure that it's
|
||
-- always a list
|
||
if type(notes) ~= "table" then
|
||
notes = { notes }
|
||
end
|
||
-- 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)
|
||
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
|
||
if not legato then
|
||
notes_off(ts)
|
||
end
|
||
if next(notes) ~= nil then
|
||
if legato then
|
||
notes_off(ts)
|
||
end
|
||
for i, num in ipairs(notes) do
|
||
if debug >= 3 then
|
||
print("note on", num, vel)
|
||
end
|
||
midiout[k] = { time = ts, data = { 0x90+chan, num, vel } }
|
||
k = k+1
|
||
end
|
||
last_notes = notes
|
||
last_chan = chan
|
||
if gate < 1 and not legato then
|
||
-- Set the sample time at which the note-offs are due.
|
||
off_gate = gate_ts
|
||
else
|
||
-- Otherwise don't set the off time in which case the
|
||
-- note-offs gets triggered automatically above.
|
||
off_gate = nil
|
||
end
|
||
end
|
||
::skip::
|
||
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
|