diff --git a/share/scripts/barlow_arp.lua b/share/scripts/barlow_arp.lua new file mode 100644 index 0000000000..9fc3c6c69b --- /dev/null +++ b/share/scripts/barlow_arp.lua @@ -0,0 +1,751 @@ +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: ") + 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 diff --git a/share/scripts/raptor_arp.lua b/share/scripts/raptor_arp.lua new file mode 100644 index 0000000000..f1225c5ab1 --- /dev/null +++ b/share/scripts/raptor_arp.lua @@ -0,0 +1,2139 @@ +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 , 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 + +-- 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]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 + -- 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.max(1, 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 diff --git a/share/scripts/simple_arp.lua b/share/scripts/simple_arp.lua new file mode 100644 index 0000000000..771fe2655f --- /dev/null +++ b/share/scripts/simple_arp.lua @@ -0,0 +1,519 @@ +ardour { + ["type"] = "dsp", + name = "Arpeggiator", + category = "Effect", + author = "Albert Gräf", + license = "MIT", + description = [[simple_arp v0.3 + +Simple monophonic arpeggiator example with sample-accurate triggering, demonstrates how to process the new time_info data along with BBT info from Ardour's tempo map. +]] +} + +-- Copyright (c) 2023 Albert Gräf, MIT License + +-- The arpeggiator takes note input and constructs a new cyclic pattern each +-- time the input chord changes. Notes from the pattern are triggered at each +-- beat as transport is rolling. The plugin adjusts to the current time +-- signature, and also lets you subdivide the base pulse of the meter with a +-- control parameter in the setup. Note velocities for the different levels +-- can be adjusted in the setup as well. + +-- NOTE: The scheme for varying note velocities in order to create rhythmic +-- accents is a bit on the simplistic side and only provides three distinct +-- velocity levels (bar, beat, and subdivision pulses). See barlow_arp.lua for +-- a more sophisticated implementation which uses Barlow's indispensability +-- formula. + +-- The octave range can be adjusted up and down in the setup, notes from the +-- input chord are then repeated in the lower and/or upper octaves. The usual +-- pattern types are supported and can be selected in the setup: up, down, +-- up-down (exclusive and inclusive modes), order (notes are played in the +-- order in which they are input), and random. + +-- The length of the notes can be set using the gate control as a fraction +-- (0..1 value) of the note division. The swing control lets you delay the +-- off-beat notes by varying amounts, given as a fraction ranging from 0.5 to +-- 0.75; a value of 0.5 produces a straight rhythm (no swing), 0.67 a triplet +-- feel. + +-- A toggle in the setup lets you enable latch mode, in which the current +-- pattern keeps playing if you release all keys, until you start a new +-- chord. Another toggle enables sync mode, in which the pattern is properly +-- synchronized to bars and beats, no matter where you change chords. This +-- also works with patterns spanning multiple bars, and often creates a much +-- smoother arpeggio than just cycling through the pattern (which is the +-- default). Both latch and sync mode are especially helpful for imprecise +-- players (like me) who tend to miss beats in chord changes. + +-- The bypass toggle, when engaged, suspends arpeggiator playback and sends +-- through the input notes as they are. This is intended to monitor the input +-- going into the arpeggiator, but can also be used as a performance tool. +-- (Disabling the arpeggiator plugin in Ardour has a similar effect, but +-- doesn't silence existing notes, which the bypass toggle does.) + +-- All these parameters are plugin controls which can be automated and saved +-- in presets. Some factory presets are provided as well. + +-- Last but not least, the plugin listens on all MIDI channels, and the last +-- MIDI channel used in the input also sets the MIDI channel for output. This +-- lets you play drumkits which expect their MIDI input on a certain MIDI +-- channel (usually channel 10), without having to fiddle with Ardour's MIDI +-- track parameters, provided that your MIDI controller can send data on the +-- appropriate MIDI channel. + +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 = 16, 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 = "Velocity 1", min = 0, max = 127, default = 100, integer = true, doc = "velocity level (bar)" }, + { type = "input", name = "Velocity 2", min = 0, max = 127, default = 80, integer = true, doc = "velocity level (beat)" }, + { type = "input", name = "Velocity 3", min = 0, max = 127, default = 60, integer = true, doc = "velocity level (subdivision)" }, + { 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 } }, + { type = "input", name = "Swing", min = 0.5, max = 0.75, default = 0.5, doc = "swing factor (0.67 = triplet feel)" }, + } +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, ["Velocity 1"] = 100, ["Velocity 2"] = 80, ["Velocity 3"] = 60, Latch = 0, Sync = 0, Swing = 0.5, 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, Swing = 0.5, Gate = 1 } }, + { name = "4 swing 60% #1 - synth", params = { Division = 2, ["Octave up"] = 1, ["Octave down"] = 1, Pattern = 3, Swing = 0.6, Gate = 1 } }, + { name = "5 swing 60% #2 - drums", params = { Division = 2, ["Octave up"] = 0, ["Octave down"] = 0, Pattern = 1, Swing = 0.6, Gate = 1 } }, + { name = "6 swing 66% #1 - synth", params = { Division = 2, ["Octave up"] = 1, ["Octave down"] = 1, Pattern = 3, Swing = 0.66, Gate = 1 } }, + { name = "7 swing 66% #2 - drums", params = { Division = 2, ["Octave up"] = 0, ["Octave down"] = 0, Pattern = 1, Swing = 0.66, Gate = 1 } }, + } +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_time -- last beat number and sample time +local last_num -- last note +local last_chan -- MIDI channel of last note +local last_gate -- off time of last note +local swing_time -- sample time of delayed pulse (swing) +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) + +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 vel1, vel2, vel3 = math.floor(ctrl[5]), math.floor(ctrl[6]), math.floor(ctrl[7]) + local latch = ctrl[8] > 0 + local sync = ctrl[9] > 0 + local bypass = ctrl[10] > 0 + local gate = ctrl[11] + -- It seems customary to specify swing using a percentage (or fraction) + -- where 50% = 1/2 denotes a straight rhythm (no swing) and 67% = 2/3 a + -- triplet feel. Here we translate this to a swing factor which is + -- multiplied with the note division time to give the timing of the + -- off-beat notes. + local swing = 1+2*(ctrl[12]-0.5) + -- 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 () + 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 + + if swing == 1 then + swing_time = nil + 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 + swing_time = nil + 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 + 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: ") + 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_time and time.sample < last_time then + -- wrap-around (probably during a loop) + swing_time = nil + end + if swing_time and swing_time >= time.sample then + if swing_time < time.sample_end then + bt, ts = time.beat, swing_time + end + elseif last_beat ~= math.floor(time.beat) or bf1 == b1 then + -- sudden jump in transport => 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) + -- same for sample time, to detect wrap-around + last_time = time.sample + -- 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) + -- duration of this step + local dur = tm:bbt_duration_at(pos, Temporal.BBT_Offset(0,1,0)):samples() / subdiv + -- next note offset in swing mode + local swing_dur = math.floor(dur * swing) + local swing_ts = ts + swing_dur + -- calculate the note-off time in samples, this is used if the gate + -- control is neither 0 nor 1 + local gate_dur = math.floor(dur * gate) + -- adjust the gate duration for swing + if swing > 1 then + if swing_time then + gate_dur = gate_dur - math.floor(dur * (swing-1) * gate) + else + gate_dur = gate_dur + math.floor(dur * (swing-1) * gate) + end + end + local gate_ts = ts + gate_dur + 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 + 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 + 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 + -- Calculate a basic velocity pattern: by default, 100 for the + -- first beat in a bar, 80 for the other non-fractional beats, 60 + -- for everything else (subdivision pulses). These values can be + -- changed with the corresponding control. NOTE: There are much + -- more sophisticted ways to do this, but we try to keep things + -- simple here. + local v = vel3 + if p == 0 then + v = vel1 + elseif p == math.floor(p) then + v = vel2 + end + --print("p", p, "v", v) + -- trigger the new note + if sync then + -- sync pattern to the bbt + local mdiv = meter:divisions_per_bar() + local npulses = mdiv * subdiv + local l = #pattern + local k = math.floor(p*subdiv) -- 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 + -- we take a very small gate value (close to 0) to mean legato + -- instead, which means the same as a 1 gate value here + local legato = gate_ts < time.sample_end + 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 + if swing_time or swing == 1 then + swing_time = nil + else + if debug >= 4 then + print("swing", swing_dur) + end + swing_time = swing_ts + end + end + end + else + -- transport not rolling or bypass; reset all cached status information + last_beat, last_time = nil, nil + swing_time = 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