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, regular_block_length = 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