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: <empty>")
	 end
      end
   end

   if rolling and not bypass then
      -- transport is rolling, not bypassed, so the arpeggiator is playing
      if last_gate and last_num and
	 last_gate >= time.sample and last_gate < time.sample_end then
	 -- Gated notes don't normally fall on a beat, so we detect them
	 -- here. (If the gate time hasn't been set or we miss it, then the
	 -- note-off will be taken care of when the next note gets triggered,
	 -- see below.)
	 if debug >= 3 then
	    print("note off", last_num)
	 end
	 -- sample-accurate "off" time
	 local ts = last_gate - time.sample + 1
	 midiout[k] = { time = ts, data = { 0x80+last_chan, last_num, 100 } }
	 last_num = nil
	 k = k+1
      end
      -- Check whether a beat is due, so that we trigger the next note. We
      -- want to do this in a sample-accurate manner in order to avoid jitter,
      -- which makes things a little complicated.  There are three cases to
      -- consider here:
      -- (1) Transport just started rolling or the playhead moved for some
      -- reason, in which case we *must* output the note immediately in order
      -- to not miss a beat (even if we're a bit late).
      -- (2) The beat occurs exactly at the beginning of a processing cycle,
      -- so we output the note immediately.
      -- (3) The beat happens some time during the cycle, in which case we
      -- calculate the sample at which the note is due.
      local denom = time.ts_denominator * subdiv
      -- beat numbers at start and end, scaled by base pulses and subdivisions
      local b1, b2 = denom/4*time.beat, denom/4*time.beat_end
      -- integral part of these
      local bf1, bf2 = math.floor(b1), math.floor(b2)
      -- sample times at start and end
      local s1, s2 = time.sample, time.sample_end
      -- current (nominal, i.e., unscaled) beat number, and its sample time
      local bt, ts
      if last_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
