Development of "VPDpro. Metron". Some demostrations

Finally, I have added a window module to the VPDpro family, called “Metron”. It is a virtual metronome :o, without sound, only visual.This is the aspect of the prototype:

7748 vpdpro-metronome-2.png

The operation is very simple. The metronome is activated when the song is played, and stops when the song is stopped from the tool:

7747 vpdpro-metronome-1.gif

It is possible to regulate the speed, latency and also the compass of x2, x3, and x4.

I probably change the names. For example, instead of “Tempo”, I change it for “Beats”:

  1. Compas
  2. Beats (refers to the metronome rod)
  3. Latency

Anyone would think how to improve this?

EDIT:I have updated the name of “Metronome” for " Metron".

The code of Metron (in diapers):

Click to view contents
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
---METRON
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------

VPD_MTR_CLR = { {000,000,000},{229,160,032} }

--miliseconds
function vpd_mtr_ms( var, lat, bps, lps, nol, ms )
  var = vb.views["VPD_MTR_VB_BEATS"].value
  lat = vb.views["VPD_MTR_VB_LATENCY"].value
  bps = renoise.song().transport.bpm / 60
  lps = renoise.song().transport.lpb * bps
  nol = renoise.song().selected_pattern.number_of_lines
  ms = (1000 + lat ) * ( 1 / lps ) * ( nol / var )
  return ms
end

--compas 2
function vpd_mtr_timer_x2_1()
  vb.views["VPD_MTR_BT_X2_1"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x2_1 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x2_1 )
    vpd_mtr_timer_x2_2()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x2_1, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X2_1"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x3_ico.png"
  end
end
---
function vpd_mtr_timer_x2_2()
  vb.views["VPD_MTR_BT_X2_2"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x2_2 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x2_2 )
    vpd_mtr_timer_x2_1()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x2_2, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X2_2"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x4_ico.png"
  end
end

--compas 3
function vpd_mtr_timer_x3_1()
  vb.views["VPD_MTR_BT_X3_1"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x3_1 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x3_1 )
    vpd_mtr_timer_x3_2()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x3_1, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X3_1"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x5_ico.png"
  end
end
---
function vpd_mtr_timer_x3_2()
  vb.views["VPD_MTR_BT_X3_2"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x3_2 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x3_2 )
    vpd_mtr_timer_x3_3()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x3_2, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X3_2"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x4_ico.png"
  end
end
---
function vpd_mtr_timer_x3_3()
  vb.views["VPD_MTR_BT_X3_3"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x3_3 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x3_3 )
    return
    vpd_mtr_timer_x3_1()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x3_3, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X3_3"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x3_ico.png"
  end
end

--compas 4
function vpd_mtr_timer_x4_1()
  vb.views["VPD_MTR_BT_X4_1"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x4_1 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_1 )
    vpd_mtr_timer_x4_2()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x4_1, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X4_1"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x1_ico.png"
  end
end
---
function vpd_mtr_timer_x4_2()
  vb.views["VPD_MTR_BT_X4_2"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x4_2 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_2 )
    vpd_mtr_timer_x4_3()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x4_2, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X4_2"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x2_ico.png"
  end
end
---
function vpd_mtr_timer_x4_3()
  vb.views["VPD_MTR_BT_X4_3"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x4_3 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_3 )
    return
    vpd_mtr_timer_x4_4()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x4_3, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X4_3"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x3_ico.png"
  end
end
---
function vpd_mtr_timer_x4_4()
  vb.views["VPD_MTR_BT_X4_4"].color = VPD_MTR_CLR[1]
  if renoise.tool():has_timer( vpd_mtr_timer_x4_4 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_4 )
    vpd_mtr_timer_x4_1()
  else
    renoise.tool():add_timer( vpd_mtr_timer_x4_4, vpd_mtr_ms() )
    vb.views["VPD_MTR_BT_X4_4"].color = VPD_MTR_CLR[2]
    vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x4_ico.png"
  end
end

--reset all timmers
function vpd_mtr_reset_timers()
  --x2
  if renoise.tool():has_timer( vpd_mtr_timer_x2_1 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x2_1 )
  end
  if renoise.tool():has_timer( vpd_mtr_timer_x2_2 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x2_2 )
  end
  --x3
  if renoise.tool():has_timer( vpd_mtr_timer_x3_1 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x3_1 )
  end
  if renoise.tool():has_timer( vpd_mtr_timer_x3_2 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x3_2 )
  end
  if renoise.tool():has_timer( vpd_mtr_timer_x3_3 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x3_3 )
  end
  --x4
  if renoise.tool():has_timer( vpd_mtr_timer_x4_1 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_1 )
  end
  if renoise.tool():has_timer( vpd_mtr_timer_x4_2 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_2 )
  end
  if renoise.tool():has_timer( vpd_mtr_timer_x4_3 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_3 )
  end
  if renoise.tool():has_timer( vpd_mtr_timer_x4_4 ) then
    renoise.tool():remove_timer( vpd_mtr_timer_x4_4 )
  end
  for i = 1, 2 do
    vb.views["VPD_MTR_BT_X2_"..i].color = VPD_MTR_CLR[1]
  end
  for i = 1, 3 do
    vb.views["VPD_MTR_BT_X3_"..i].color = VPD_MTR_CLR[1]
  end
  for i = 1, 4 do
    vb.views["VPD_MTR_BT_X4_"..i].color = VPD_MTR_CLR[1]
  end
end

--start
function vpd_mtr_start()
  vpd_mtr_reset_timers()
  play_mode = renoise.Transport.PLAYMODE_RESTART_PATTERN 
  renoise.song().transport:start( play_mode )
    
  if ( vb.views["VPD_MTR_VB_COMPAS"].value == 2 and vb.views["VPD_MTR_RW_X2"].visible == true ) then
    vpd_mtr_timer_x2_1()
  end 
  if ( vb.views["VPD_MTR_VB_COMPAS"].value == 3 and vb.views["VPD_MTR_RW_X3"].visible == true ) then
    vpd_mtr_timer_x3_1()
  end
  if ( vb.views["VPD_MTR_VB_COMPAS"].value == 4 and vb.views["VPD_MTR_RW_X4"].visible == true ) then
    vpd_mtr_timer_x4_1()
  end    
end

--stop
function vpd_mtr_stop()
  renoise.song().transport:stop()
  renoise.song().selected_line_index = 1
  vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./icons/metron/metron_x0_ico.png"
end

--reset
function vpd_mtr_reset()
  vb.views["VPD_MTR_VB_COMPAS"].value = 4
  vb.views["VPD_MTR_VB_BEATS"].value = 16
  vb.views["VPD_MTR_VB_LATENCY"].value = 8
end

--monitor '1,2,3,4..1,2..'
function vpd_mtr_monitor( value )
  if value == 2 then
  vb.views["VPD_MTR_RW_X4"].visible = false
  vb.views["VPD_MTR_RW_X3"].visible = false
  vb.views["VPD_MTR_RW_X2"].visible = true
  end
  if value == 3 then
  vb.views["VPD_MTR_RW_X4"].visible = false
  vb.views["VPD_MTR_RW_X2"].visible = false
  vb.views["VPD_MTR_RW_X3"].visible = true  
  end
  if value == 4 then
  vb.views["VPD_MTR_RW_X2"].visible = false
  vb.views["VPD_MTR_RW_X3"].visible = false
  vb.views["VPD_MTR_RW_X4"].visible = true
  end
end

--GUI
---------------------------------------------------------------
metron_content = vb:row { margin = 3,
  vb:column { margin = 3, style = "plain",
    vb:row {
      vb:column { height = 101, width = 131,
       vb:bitmap { id = "VPD_MTR_BM_METRONOME", mode = "transparent", bitmap = "./icons/metron/metron_x0_ico.png" }
      },
      vb:column {
        vb:row {
          vb:text { height = 21, width = 50, align = "right", text = "Compas" },
          vb:valuebox { id = "VPD_MTR_VB_COMPAS", height = 21, width = 50, min = 2, max = 4, value = 4, notifier = function( value ) vpd_mtr_monitor( value ) end }
        },
        vb:row {
          vb:text { height = 21, width = 50, align = "right", text = "Beats" },
          vb:valuebox { id = "VPD_MTR_VB_BEATS", height = 21, width = 50, min = 1, max = 32, value = 16, notifier = function() end }
        },
        vb:row {
          vb:text { height = 21, width = 50, align = "right", text = "Latency" },
          vb:valuebox { id = "VPD_MTR_VB_LATENCY", height = 21, width = 50, min = 00, max = 50, value = 8, notifier = function() end }
        }
      }
    },
    vb:row { id = "VPD_MTR_RW_X2", spacing = -3, visible = false,
      vb:button { id = "VPD_MTR_BT_X2_1", height = 21, width = 117, text = "1" },
      vb:button { id = "VPD_MTR_BT_X2_2", height = 21, width = 117, text = "2" },
    },
    vb:row { id = "VPD_MTR_RW_X3", spacing = -3, visible = false,
      vb:button { id = "VPD_MTR_BT_X3_1", height = 21, width = 79, text = "1" },
      vb:button { id = "VPD_MTR_BT_X3_2", height = 21, width = 79, text = "2" },
      vb:button { id = "VPD_MTR_BT_X3_3", height = 21, width = 79, text = "3" },
    },
    vb:row { id = "VPD_MTR_RW_X4", spacing = -3, visible = true,
      vb:button { id = "VPD_MTR_BT_X4_1", height = 21, width = 60, text = "1" },
      vb:button { id = "VPD_MTR_BT_X4_2", height = 21, width = 60, text = "2" },
      vb:button { id = "VPD_MTR_BT_X4_3", height = 21, width = 60, text = "3" },
      vb:button { id = "VPD_MTR_BT_X4_4", height = 21, width = 60, text = "4" },
    },
    vb:row {
      vb:button { id = "VPD_MTR_BT_START", height = 21, width = 77, text = "Start", notifier = function() vpd_mtr_start() end },
      vb:button { id = "VPD_MTR_BT_STOP", height = 21, width = 77, text = "Stop", notifier = function() vpd_mtr_reset_timers() vpd_mtr_stop() end },
      vb:button { id = "VPD_MTR_BT_RESET", height = 21, width = 77, text = "Reset", notifier = function() vpd_mtr_reset() end },
    }
  }
}

function vpd_invokes_metron_window()
  dialog_metron()
end
metron_dialog = nil
function dialog_metron()
  if ( metron_dialog and metron_dialog.visible ) then metron_dialog:show() return metron_dialog:close() , foreground_dialog() end
  metron_dialog = renoise.app():show_custom_dialog( " ▛ VPDpro. Metron", metron_content, key_handler )
end

In case someone wants experimental with him (for the code to work correctly, it would be necessary to include the images).

Without getting into the super deep ins and outs of all this, just to quickly point out, surely it is possible to write something similar Raul with just starting and stopping the one timer function (and make the code generally a little more compact/neater)?

Besides, all those ‘Add_timer/Has_timer/Remove_timer’ lines are making my eyes go funny :blink:

Without getting into the super deep ins and outs of all this, just to quickly point out, surely it is possible to write something similar Raul with just starting and stopping the one timer function (and make the code generally a little more compact/neater)?

Besides, all those ‘Add_timer/Has_timer/Remove_timer’ lines are making my eyes go funny :blink:

Click to view contents
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
---METRON
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
renoise.tool():add_menu_entry {
name = "Main Menu:Tools:Metron",
invoke = function() vpd_invokes_metron_window() end
}
local vb = renoise.ViewBuilder()
local count = 0
local mbcount = 0
VPD_MTR_CLR = { {000,000,000},{229,160,032} }
--miliseconds
function vpd_mtr_ms( var, lat, bps, lps, nol, ms )
var = vb.views["VPD_MTR_VB_BEATS"].value
lat = vb.views["VPD_MTR_VB_LATENCY"].value
bps = renoise.song().transport.bpm / 60
lps = renoise.song().transport.lpb * bps
nol = renoise.song().selected_pattern.number_of_lines
ms = (1000 + lat ) * ( 1 / lps ) * ( nol / var )
return ms
end
function vpd_mtr_timer()
count = count + 1
if count > vb.views["VPD_MTR_VB_COMPAS"].value then count = 1 end
mbcount = mbcount + 1
if mbcount > 4 then mbcount = 1 end
local cvalue = vb.views["VPD_MTR_VB_COMPAS"].value
for i = 1, cvalue do
vb.views["VPD_MTR_BT_X"..cvalue.."_"..i].color = VPD_MTR_CLR[1]
end
vb.views["VPD_MTR_BT_X"..cvalue.."_"..count].color = VPD_MTR_CLR[2]
vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./metron_x"..mbcount.."_ico.png"
end
--start
function vpd_mtr_start()
local play_mode = renoise.Transport.PLAYMODE_RESTART_PATTERN
renoise.song().transport:start( play_mode )
local cvalue = vb.views["VPD_MTR_VB_COMPAS"].value
vb.views["VPD_MTR_BT_X"..cvalue.."_1"].color = VPD_MTR_CLR[2]
count = 1
mbcount = 0
renoise.tool():add_timer( vpd_mtr_timer, vpd_mtr_ms() )
end
--stop
function vpd_mtr_stop()
renoise.tool():remove_timer( vpd_mtr_timer )
renoise.song().transport:stop()
renoise.song().selected_line_index = 1
local cvalue = vb.views["VPD_MTR_VB_COMPAS"].value
for i = 1, cvalue do vb.views["VPD_MTR_BT_X"..cvalue.."_"..i].color = VPD_MTR_CLR[1] end
vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./metron_x0_ico.png"
end
--reset
function vpd_mtr_reset()
vb.views["VPD_MTR_VB_COMPAS"].value = 4
vb.views["VPD_MTR_VB_BEATS"].value = 16
vb.views["VPD_MTR_VB_LATENCY"].value = 8
end
--monitor '1,2,3,4..1,2..'
function vpd_mtr_monitor( value )
if value == 2 then
vb.views["VPD_MTR_RW_X4"].visible = false
vb.views["VPD_MTR_RW_X3"].visible = false
vb.views["VPD_MTR_RW_X2"].visible = true
end
if value == 3 then
vb.views["VPD_MTR_RW_X4"].visible = false
vb.views["VPD_MTR_RW_X2"].visible = false
vb.views["VPD_MTR_RW_X3"].visible = true
end
if value == 4 then
vb.views["VPD_MTR_RW_X2"].visible = false
vb.views["VPD_MTR_RW_X3"].visible = false
vb.views["VPD_MTR_RW_X4"].visible = true
end
end
--GUI
---------------------------------------------------------------
metron_content = vb:row { margin = 3,
vb:column { margin = 3, style = "plain",
vb:row {
vb:column { height = 101, width = 131,
vb:bitmap { id = "VPD_MTR_BM_METRONOME", mode = "transparent", bitmap = "./metron_x0_ico.png" }
},
vb:column {
vb:row {
vb:text { height = 21, width = 50, align = "right", text = "Compas" },
vb:valuebox { id = "VPD_MTR_VB_COMPAS", height = 21, width = 50, min = 2, max = 4, value = 4, notifier = function( value ) vpd_mtr_monitor( value ) end }
},
vb:row {
vb:text { height = 21, width = 50, align = "right", text = "Beats" },
vb:valuebox { id = "VPD_MTR_VB_BEATS", height = 21, width = 50, min = 1, max = 32, value = 16, notifier = function() end }
},
vb:row {
vb:text { height = 21, width = 50, align = "right", text = "Latency" },
vb:valuebox { id = "VPD_MTR_VB_LATENCY", height = 21, width = 50, min = 00, max = 50, value = 8, notifier = function() end }
}
}
},
vb:row { id = "VPD_MTR_RW_X2", spacing = -3, visible = false,
vb:button { id = "VPD_MTR_BT_X2_1", height = 21, width = 117, text = "1" },
vb:button { id = "VPD_MTR_BT_X2_2", height = 21, width = 117, text = "2" },
},
vb:row { id = "VPD_MTR_RW_X3", spacing = -3, visible = false,
vb:button { id = "VPD_MTR_BT_X3_1", height = 21, width = 79, text = "1" },
vb:button { id = "VPD_MTR_BT_X3_2", height = 21, width = 79, text = "2" },
vb:button { id = "VPD_MTR_BT_X3_3", height = 21, width = 79, text = "3" },
},
vb:row { id = "VPD_MTR_RW_X4", spacing = -3, visible = true,
vb:button { id = "VPD_MTR_BT_X4_1", height = 21, width = 60, text = "1" },
vb:button { id = "VPD_MTR_BT_X4_2", height = 21, width = 60, text = "2" },
vb:button { id = "VPD_MTR_BT_X4_3", height = 21, width = 60, text = "3" },
vb:button { id = "VPD_MTR_BT_X4_4", height = 21, width = 60, text = "4" },
},
vb:row {
vb:button { id = "VPD_MTR_BT_START", height = 21, width = 77, text = "Start", notifier = function() vpd_mtr_start() end },
vb:button { id = "VPD_MTR_BT_STOP", height = 21, width = 77, text = "Stop", notifier = function() vpd_mtr_stop()
end },
vb:button { id = "VPD_MTR_BT_RESET", height = 21, width = 77, text = "Reset", notifier = function() vpd_mtr_reset() end },
}
}
}
function vpd_invokes_metron_window()
dialog_metron()
end
function key_handler()
end
metron_dialog = nil
function dialog_metron()
metron_dialog = renoise.app():show_custom_dialog( " ▛ VPDpro. Metron", metron_content, key_handler )
end

Thanks 4Tey! :lol:Yes, I know I can iterate to compact the code. But the problem is that each compas, x2, x3, x4, has a different behavior in the metronome image. I would have to study deeply how to do it so as not to repeat code.Apart from all this, I have to add up to x9, and each configuration will have a different behavior.

Here is a problem

vb.views["VPD_MTR_BM_METRONOME"].bitmap = "./metron_x"..mbcount.."_ico.png"

Probably can add conditions, the code will be somewhat longer, but better than before.

I do not know if you have noticed. But this metronome, having a “custom” timer, tends to go a little faster, 2, 4, 8 ms faster than the playback speed of the song. Do you think you could determine why this happens?That’s why I added a latency…

I mean this function (the calculation of the duration for the timer):

--miliseconds
function vpd_mtr_ms( var, lat, bps, lps, nol, ms )
  var = vb.views["VPD_MTR_VB_BEATS"].value --beats valuebox [2, 3, 4, ...until 9 in the future]
  lat = vb.views["VPD_MTR_VB_LATENCY"].value --latency valuebox [range 0 to +50]
  bps = renoise.song().transport.bpm / 60 --beats per second
  lps = renoise.song().transport.lpb * bps --lines per second
  nol = renoise.song().selected_pattern.number_of_lines --number of lines of selected pattern
  ms = (1000 + lat ) * ( 1 / lps ) * ( nol / var ) --miliseconds, duration to the timer
  return ms
end

I have explained what each line is in the code… It is understood?Why is there a slight lag in time? I’m talking about 2 to 8ms for what I’ve tried. To test it, you can play a song with many patterns. At first it seems synchronized, but when playing many patterns, it desynchronizes slightly, and it shows visually.Maybe the code is wrong?

In theory, exactly when one step ends, you should start the next one. Can there be a lag of a couple of milliseconds triggered by the code?In theory it should always be accurate…

Thanks!

The API documentation states:

The exact interval your function is called will vary
a bit, depending on workload; e.g. when enough CPU time is available the
rounding error will be around +/- 5 ms.

Apperantly, it’s closely related to the app_idle_observable.

Reference: https://github.com/renoise/xrnx/blob/master/Documentation/Renoise.ScriptingTool.API.lua#L272

An idea:

Are you constantly registering/unregistering timers now? I would assume that this will cause a random drift. If you just use one timer only, I don’t believe it will drift, but just be very slightly out-of-sync thru-out - in an acceptable and constant manner.

Just hook up a relevant function to an ObservableBang, and trigger that bang from one single timer only.

(If my assumption about drifting is false, you’ll have to make some special re-adjustment scheme, adjusting the timer to a cached init value from os.clock)

The API documentation states:

Apperantly, it’s closely related to the app_idle_observable.

Reference: https://github.com/renoise/xrnx/blob/master/Documentation/Renoise.ScriptingTool.API.lua#L272

Wow, also negative? I understood that it was always a delay (positive value of +5 approximately).If there can be a delay of -5 ms, it implies that it may be the case to launch the timer 5 ms earlier, so it seems to work a little faster.I’ll have to check if it happens exactly the same with a renoise.tool (): add_timer

Is it possible that the playback of the song is actually slower? I have never timed the playback of the song with physical stopwatch.

Anyway, I think it would be possible to change the focus of the code, and that the action of the metronome would depend on the sequence of the song, that is, it is activated when a pattern starts in the sequencing, and stops and starts again, in the next pattern of the sequence. If there is a slight desynchronization, it will not be visually perceptible, even with 512 lines per pattern (maybe).As the code is now, the metronome is independent of the playback of the song. Only synchronize with the number of lines actually, to manually reset it.

The ideal is not to have to add any manually latency, but the metronome should always work synchronized according to the song…The scheme would be this:

  1. Start the song with sequence 1, with playback activated to run the song.
  2. Immediately, the timer is activated according to the configuration of the metronome.
  3. The metronome, with its independent timer, will stop at the end of the sequence 1 pattern, without depending on the playback of the song.
  4. In the pattern sequence 2,the timer starts again, as before, and will stop at the end of the pattern sequence 2 playback,without depending on the reproduction of the song.
  5. So on until the end of the song.
  • The timer will always be conditioned by:
  • Number of linesof each pattern
  • Start of each pattern according to the sequence.

I think that this approach would only drag that little error of + -5ms, invaluable because it does not creep in time, in each pattern in the sequence it would have the same timer, exactly the same behavior, without accumulating possible delays

Are you constantly registering/unregistering timers now? …

My initial code only activates once the timer, and it stops when the song stops, only once, during the entire course of the song.The way to correct it is to launch an exclusive timer for each pattern in the sequence.That is, the metronome acts on each pattern, not with a single timer for the entire song. In this way, so that a lag is not noticed, the timer would be activated every time a new pattern starts in the sequence, and it would stop at the end of the pattern that is playing at each moment.

If a pattern has a parameter to change the BPM or LPB,the metronome will desynchronize only in that pattern.At the moment I do not contemplate this scenario…

  • Start sequence 1, start timer
  • End sequence 1, end timer
  • Start sequence 2, start timer
  • End sequence 2, end timer
  • Start sequence 3, start timer
  • End sequence 3, end timer
  • etc

The timer function would be the same. To each pattern, its timer.I’ll look to build this approach, I think it would work finest…

Ah. That sounds like a good plan! I thought the drifting occured due to adding one timer via another - sequentially.

Your description is a bit wrong in the bullet list, but never mind. Just remove any existing timer before adding a new one. This only needs to happen when a new sequence is started.

However, drifting might still occur if you are looping one sequence only.

I assume that app_idle_observable is less precise? Otherwise you could use that instead of a timer, and cover all scenarios by tracking the current line index.

Yeah there will be always imprecision with current lua api, around +/- 20ms ++ !! So what is the purpose of this? I non-precise metronome??

Ah. That sounds like a good plan! I thought the drifting occured due to adding one timer via another - sequentially.

I think I have not explained myself well before. What I use is a concatenation of timers (equal functions, but they are “different timers”,each with a different delay (+ -) ). Then the following timer will take the previous timer error, which is what you comment.

I think the trick is that the timer depends on the start of each pattern in the sequence, not a previous timer. I think I already have the right approach to better adjust the metronome.

Yes, the timer will still have a small delay error when starting each pattern in the sequence, but I think it will be visually inappreciable.

Yeah there will be always imprecision with current lua api, around +/- 20ms ++ !! So what is the purpose of this? I non-precise metronome??

Theoretically, I believe that it is possible to build a metronome with such a small time error (±5ms aprox) that you will not be able to appreciate it with your lynx view. The theme is to adjust the code to make it so.

Unfortunately, there will always be some small delay when you have to execute some code. But with a well-made code, it may not be noticeable to the eye.Even if there is some small error in each pattern, it is always possible to manually add a small latency of 2 or 5 ms, since, I suppose, it also depends on your processor.

Yes, the timer will still have a small delay error when starting each pattern in the sequence, but I think it will be visually inappreciable.

IMHO you should invest your time in something more useful. How can this now help in the current state? Some nice graphics blinking? The only chance you would have to make it exact would be to calculate a timestamp offset, and then let WAIT the script for the exact moment, blocking the whole gui thread, since the lua engine is running inside that. This makes no sense at all. I am really ?? now.

No, it’s about 20ms, not 5ms. It will be better on some systems, but you have to assume the worst system of course.

IMHO you should invest your time in something more useful. How can this now help in the current state? Some nice graphics blinking? The only chance you would have to make it exact would be to calculate a timestamp offset, and then let WAIT the script for the exact moment, blocking the whole gui thread, since the lua engine is running inside that. This makes no sense at all. I am really ?? now.

No, it’s about 20ms, not 5ms. It will be better on some systems, but you have to assume the worst system of course.

I do not understand your approach very well.If you depend on an external clock, it is likely that you have synchronization problems, since the reproduction of the song has its own rhythm, its variables (number of lines per pattern, BPM, LPB …).

With the approach of activating a timer in each pattern according to the sequence, the metronome will visually work quite accurately, without dragging delays. If the observables are slower (10ms), it is possible to compensate the latency so that it is more accurate. Visually the metronome will go “exactly”, and always according to the configuration of the song.

It is necessary to thoroughly test the code to know if one is “wasting time”.

Hm ok. Maybe I am wrong then. It will be delayed by 2-3 frames maximum. But test your observable with 10ms on a Mac system or a slow system, it still will be ~20ms. That’s why I say the minimum is 20ms. A 1/60 Hz frame takes 16,6 ms, a 16th note at 120BPM is 125ms. So it would appear at the 3rd frame instead 1st, so 33ms delayed. Maybe that’s precise enough then for you. But since I can already realize an audio latency >10ms, preventing me to record fine, precise rhythmic patterns, I really wonder how this visual metronome will help at all.

  • The Renoise GUI does not ensure a stable framerate, it even could be delayed further by slowdowns.

Ok, obviously I am wrong. So it would be just one frame delayed. nvm then, please go on :slight_smile:

@ffxAs I understand it, Renoise gives priority to sound, rather than graphics. This means that the song should sound synchronized at all times, and it is the Renoise GUI that is updated in case it runs slower. That is why, in low-powered computers, the GUI may blink.

Then, the beginning of each pattern in the sequence depends on the sound of the song, not the graphics. The idea is to synchronize the metronome as best as possible to the sound of the song, and the best starting point is the beginning of each pattern in the sequence. So, if there is an error, it does not accumulate, you only reproduce a single error, and that perhaps translates into a metronome sufficiently precise to the human eye.

In theory, if the song’s sound has latency, the metronome’s timer will act according to the sound of the song, including said latency.

Now I am in the experimentation phase, trying to understand where all the problems related to time are for a more precise adjustment (and in this adjustment I consider a small delay error at all times, which is inevitable). That’s why even the metronome tool can include a manual setting called latency (metronome latency), although it should not be necessary.

Now I am considering at all times limiting the timer by pattern, so the problem is not to adjust the timer to the whole song, but to depend only on each pattern in the sequence.

I have tested my initial code thoroughly, and it works reasonably well. In addition, the code does not consume hardly any resources, so it depends directly on two things:

  1. A delay in the execution of a function (a timer), which can be + -5ms (this is a range of 10ms of lag, -5 … 0 … +5 ms).Renoise.ScriptingTool.API.lua:
--[[

Register a timer function or table with a function and context (a method)
that periodically gets called by the app_idle_observable for your tool.

Modal dialogs will avoid that timers are called. To create a one-shot timer,
simply call remove_timer at the end of your timer function. Timer_interval_in_ms
must be > 0. The exact interval your function is called will vary
a bit, depending on workload; e.g. when enough CPU time is available the
rounding error will be around +/- 5 ms.

]]

-- Returns true when the given function or method was registered as a timer.
renoise.tool():has_timer(function or {object, function} or {function, object})
  -> [boolean]

-- Add a new timer as described above.
renoise.tool():add_timer(function or {object, function} or {function, object},
  timer_interval_in_ms)

-- Remove a previously registered timer.
renoise.tool():remove_timer(timer_func)

2.Delay in observables,to read the BPM and LPB values, the number of lines per pattern, and the beginning of each pattern in the sequence, which can be 10 ms This is precisely what I have not yet tried. Therefore, my initial code does not use observables yet…

-- Invoked periodically in the background, more often when the work load
-- is low, less often when Renoise's work load is high.
-- The exact interval is undefined and can not be relied on, but will be
-- around 10 times per sec.
-- You can do stuff in the background without blocking the application here.
-- Be gentle and don't do CPU heavy stuff please!
renoise.tool().app_idle_observable
  -> [renoise.Document.Observable object]

The doubt that I have now is:There is some delay in usingrenoise.song().selected_sequence_index_observable:add:notifier(function)???

-- The currently edited sequence position.
renoise.song().selected_sequence_index, _observable
  -> [number]

The code must check the sequence index number at all times. The same for this:

-- BPM, LPB, and TPL.
renoise.song().transport.bpm, _observable
  -> [number, 32-999]
renoise.song().transport.lpb, _observable
  -> [number, 1-256]

The documentation for the Observables, Renoise,Document.API.lua:

-------- Observables

Documents and Views in the Renoise API are modelled after the observer pattern
(have a look at <http://en.wikipedia.org/wiki/Observer_pattern> if this is new
to you). This means, in order to track changes, a document is basically just a
set of raw data (booleans, numbers, lists, nested nodes) which anything can
attach notifier function (listeners) to. For example, a view in the Renoise
API is an Observer, which listens to observable values in Documents.

Attaching and removing notifiers can be done with the functions 'add_notifier',
'remove_notifier' from the Observable base class. These support multiple kinds
of callbacks, plain functions and methods (functions with a context). Please
see renoise.Document.Observable for more details. Here is a simple example:

    function bpm_changed()
      print(("something changed the BPM to %s"):format(
        renoise.song().transport.bpm))
    end

    renoise.song().transport.bpm_observable:add_notifier(bpm_changed)
    -- later on, maybe:
    renoise.song().transport.bpm_observable:remove_notifier(bpm_changed)

When adding notifiers to lists (like the track list in a song) an additional
context parameter is passed to your notifier function. This way you know what
happened to the list:

    function tracks_changed(notification)
      if (notification.type == "insert") then
        print(("new track was inserted at index: %d"):format(notification.index))

      elseif (notification.type == "remove") then
        print(("track got removed from index: %d"):format(notification.index))

      elseif (notification.type == "swap") then
        print(("track at index: %d and %d swapped their positions"):format(
          notification.index1, notification.index2))
      end
    end

    renoise.song().tracks_observable:add_notifier(tracks_changed)

If you only want to use the existing "_observables" in the Renoise API,
then this is all you need to know. If you want to create your own documents,
read on.

So a call of this type has some delay too? For example:renoise.song().transport.bpm_observable:add_notifier(bpm_changed)Is there a delay in updating the value after being read, or is it instantaneous?This would be required once when starting the new pattern in the sequence…

7750 metron-sequence.png

This is a visual scheme of the approach…

Anyone would think how to improve this?

You didn’t really say anything about why you’ve started working on this thing, so I’m actually just wondering what is the intended purpose behind it?

Assuming that you do manage to figure out the timing issues that have been discussed, why would anyone need a purely visual metronome?

At least for me personally, whenever I use the metronome I’m simply listening to it while I focus on playing/performing, but I’ve never wanted to actually see a graphical representation of the metronome itself.

So is this purely a visual gimmick for fun, or is there a greater purpose behind it?

You didn’t really say anything about why you’ve started working on this thing, so I’m actually just wondering what is the intended purpose behind it?

Assuming that you do manage to figure out the timing issues that have been discussed, why would anyone need a purely visual metronome?

At least for me personally, whenever I use the metronome I’m simply listening to it while I focus on playing/performing, but I’ve never wanted to actually see a graphical representation of the metronome itself.

So is this purely a visual gimmick for fun, or is there a greater purpose behind it?

Both things!I like to experiment with the code. But also a visual metronome can help me in certain compositions. On the other hand, I also want to build it, to see if it can really be built with precision or not.I can be guided by hate, and in certain cases by sight, and concentrate the ear on something else.

So the purpose is multiple.You can also think that creating the code is fun. The process can help build other things later.

creating the code is fun

A perfectly good reason :slight_smile: