Starting with LUA. Beat Flash Script

hi all!!!
I start to learn lua with ardour. I begin to read the docs, examples and help me with IA to understand how to it works.

I found a post confirming that I have to restart Ardour to reload the script if I make changes.
This is very inconvenient during the development process.
It would be great if Ardour had a “Developer mode” option for these situations, to detect script changes and incorporate the new modifications. It wouldn’t matter if it added some processing overhead (or resulted in some optimization loss), as it would only be for script development and not for actual production.

I recently acquired a Novation Launchkey 49 MK2, and this script sends the tempo beat to the first four buttons that light up, essentially creating a visual metronome.

I hope it helps others as it helped me to start learning Lua.

ardour {
  ["type"]  = "dsp",
  name      = "Beat Pad Flasher",
  author    = "ragnarok@docksud.com.ar",
  category  = "Utility",
  description = [[
Metrónomo visual para Launchkey MK2 49.
Beat 1 del compás = ROJO. Beats 2/3/4 = VERDE en los 4 pads cuadrados.

SETUP:
1. Crear un Instrument Track (sin instrumento).
2. Agregar este plugin al track.
3. Conectar salida MIDI del track a "Launchkey MK2 49 Launchkey InCo".
4. Play → pads parpadean al ritmo.
]]
}

function dsp_ioconfig ()
  return { { midi_in = 1, midi_out = 1, audio_in = 0, audio_out = 0 } }
end

function dsp_options ()
  return { time_info = true }
end

-- ============================================================
-- Configuración del Launchkey MK2
-- ============================================================
local PAD_NOTES   = { 96, 97, 98, 99 }  -- 4 pads cuadrados inferiores
local COLOR_BEAT1 = 5    -- Rojo  (beat 1 del compás)
local COLOR_OTHER = 21   -- Verde (beats 2, 3, 4)
local FLASH_FRAC  = 0.4  -- El pad se apaga al 40% del beat

-- ============================================================
-- Estado global
-- ============================================================
local srate       = 48000
local last_beat   = nil
local was_rolling = false
local needs_init  = true
local pad_lit     = { false, false, false, false }
local pad_off_at  = { 0, 0, 0, 0 }  -- sample absoluto de apagado

function dsp_init (rate)
  srate       = rate
  needs_init  = true
  last_beat   = nil
  was_rolling = false
  for i = 1, 4 do
    pad_lit[i]    = false
    pad_off_at[i] = 0
  end
end

-- ============================================================
-- Loop principal DSP
-- ============================================================
function dsp_run (_, _, n_samples)
  assert (type(midiout) == "table")
  assert (type(time)    == "table")

  local k = 1

  -- Enviar Extended Mode + activar pads InControl al arrancar
  if needs_init then
    midiout[k] = { time = 1, data = { 0x9F, 0x0C, 0x7F } }  -- Extended Mode ON
    k = k + 1
    midiout[k] = { time = 2, data = { 0x9F, 0x0F, 0x7F } }  -- Pads → InControl
    k = k + 1
    needs_init = false
    return
  end

  -- Pass-through del MIDI entrante
  for _, ev in ipairs(midiin) do
    midiout[k] = ev
    k = k + 1
  end

  -- Estado del transporte
  local rolling = Session:transport_state_rolling()

  -- Se detuvo → apagar todos los pads
  if was_rolling and not rolling then
    for i, note in ipairs(PAD_NOTES) do
      if pad_lit[i] then
        midiout[k] = { time = 1, data = { 0x9F, note, 0 } }
        k = k + 1
        pad_lit[i] = false
      end
    end
    last_beat = nil
  end
  was_rolling = rolling

  if not rolling then return end

  -- Obtener BBT desde el tempo map
  local tm     = Temporal.TempoMap.read()
  local pos    = Temporal.timepos_t(time.sample)
  local bbt    = tm:bbt_at(pos)
  local beat   = bbt.beats   -- beat dentro del compás (1, 2, 3...)

  -- Calcular duración del flash en samples
  local tempo_qpm  = tm:tempo_at(pos):quarter_notes_per_minute()
  local beat_samps = math.floor((60.0 / tempo_qpm) * srate * FLASH_FRAC)

  -- Pad correspondiente al beat (cicla 1..4)
  local pad_idx = ((beat - 1) % 4) + 1
  local color   = (beat == 1) and COLOR_BEAT1 or COLOR_OTHER

  -- Nuevo beat → encender pad
  if beat ~= last_beat then
    last_beat = beat

    -- Apagar pads anteriores
    for i, note in ipairs(PAD_NOTES) do
      if pad_lit[i] and i ~= pad_idx then
        midiout[k] = { time = 1, data = { 0x9F, note, 0 } }
        k = k + 1
        pad_lit[i] = false
      end
    end

    -- Encender pad actual
    midiout[k] = { time = 1, data = { 0x9F, PAD_NOTES[pad_idx], color } }
    k = k + 1
    pad_lit[pad_idx]    = true
    pad_off_at[pad_idx] = time.sample + beat_samps
  end

  -- Apagar pads que cumplieron el flash
  local cycle_end = time.sample + n_samples
  for i, note in ipairs(PAD_NOTES) do
    if pad_lit[i] then
      local off_at = pad_off_at[i]
      if off_at < cycle_end then
        local t_off = off_at - time.sample + 1
        if t_off < 1 then t_off = 1 end
        midiout[k] = { time = t_off, data = { 0x9F, note, 0 } }
        k = k + 1
        pad_lit[i] = false
      end
    end
  end

end

3 Likes

Sounds like lua should really run in a Read Eval Print Loop.
There is a project on Github, which seems to tackle exactly the situation (running lua inside an application), but it is somewhat old and seemingly unmaintained. (GitHub - hoelzro/lua-repl: A Lua REPL implemented in Lua for embedding in other programs · GitHub)
And then, i have no fscking clue about lua and the details and woes about the implementation inside ardour. So, i better shut up and dream sweet dreams about the Lisp REPL in private.