[Solved] Help: check notes with follow the player position in Patt. Ed

  1. It seems to me like everyone is forgetting the biggest optimization of them all here - line.is_empty. This happens so frequently and is so fast to check, that a special case in the if statement should be made for this condition.

  2. Also, if line is not empty, initially checking note_column.is_empty should probably be faster than making a string comparison on each and every ncol. The only difference between “—” and note_colum.is_empty is that the latter includes dly/pan/fx-column et c. That doesn’t matter in this case, though… You kind of gain by adding a check for “not note_column.is_empty” outside here, just because it happens so often.

Do these two to avoid a lot of extra calls… The other suggestions are good as well, but these are the real heavy ones.

^^ ^^ ^_^,I thought of a patch which solves the continuous error of the first line.I do not like the result of the set code, but at least it works:

--IMPORTANT NOTE, Renoise v3.1 not read correctly the fist line of each pattern using a timer or idle_obserble. Is necessary use a filter for line 01.

--check first line only of each pattern (filter)
function tnc_check_pattern_first_line( song, tool, spi, sti, stpl )
  song = renoise.song()
  tool = renoise.tool()
  if vb.views['TNC_CB'].value == true then  
    local function check_pattern()
      
      spi, sti, stpl = song.selected_pattern_index, song.selected_track_index, song.transport.playback_pos.line
  
      if song.selected_pattern_index == spi then
        --print ( "spi:", spi )
        --print( "note line 01", song:pattern( spi ):track( sti ):line( 1 ):note_column( 6 ).note_string )
        
        --- ---
        for i = 1, 12 do
          if not ( song:pattern( spi ):track( sti ):line( 1 ).is_empty ) then
            -- line 01, note column i
            --if song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string ~= '---' then
            if not ( song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).is_empty ) then
              if ( song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).note_value < 120 ) then -- 120 = OFF, 121 = empty
                --vb.views["TNC_NC_"..i..""].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string
                vb.views["TNC_NC_"..i..""].text = song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).note_string
                vb.views["TNC_NC_"..i..""].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
              end
            else
            end
            --if song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string == 'OFF' then
            if song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).note_string == 'OFF' then
              vb.views["TNC_NC_"..i..""].text = 'OFF'
              vb.views["TNC_NC_"..i..""].color = { 0x40,0x00,0x00 }
            end
          end
          vb.views["TN_NC_NM_"..i..""].text = string.format(" %s", song:pattern( spi ):track( sti ):line( 1 ):note_column( 1 ).note_string )
        end
        --- ---
              
      end
    end
    song.selected_pattern_index_observable:add_notifier( check_pattern ) --observable for pattern
    tool.app_new_document_observable:add_notifier( tnc_check_pattern_first_line )
  else
    if tool.app_new_document_observable:has_notifier( tnc_check_pattern_first_line ) then
      tool.app_new_document_observable:remove_notifier( tnc_check_pattern_first_line )
    end
  end
end

--- ---

--tnc_change_buttons ( with timer: line_timer() ). Read all the lines except the first.
function tnc_change_buttons( song, spi, sti, stpl, i, note_01 )
  song = renoise.song()
  spi, sti, stpl = song.selected_pattern_index, song.selected_track_index, song.transport.playback_pos.line
  if song.selected_track.type == renoise.Track.TRACK_TYPE_SEQUENCER then

    --[[
    -- note column 1. Example:
    if song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string ~= '---' then
      vb.views["TNC_NC_01"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string
      vb.views["TNC_NC_01"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
    end
    if song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string == 'OFF' then
      vb.views["TNC_NC_01"].color = { 0x40,0x00,0x00 }
    end  
    ]]
  
    --- ---
    if not (song.selected_line_index == 1 ) then
      for i = 1, 12 do
        if not ( song.selected_line.is_empty ) then
          -- note column i
          --if song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string ~= '---' then
          if not ( song.selected_line:note_column( i ).is_empty ) then
            --if ( song.selected_line:note_column( i ).note_string ~= '---' ) then
            if ( song.selected_line:note_column( i ).note_value < 120 ) then
              --vb.views["TNC_NC_"..i..""].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string
              vb.views["TNC_NC_"..i..""].text = song.selected_line:note_column( i ).note_string
              vb.views["TNC_NC_"..i..""].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
            end
          else
          end
          --if song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string == 'OFF' then
          if song.selected_line:note_column( i ).note_string == 'OFF' then
            vb.views["TNC_NC_"..i..""].text = 'OFF'
            vb.views["TNC_NC_"..i..""].color = { 0x40,0x00,0x00 }
          end
        end
        vb.views["TN_NC_NM_"..i..""].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string )
      end
    end
    --- ---
    
    vb.views["TNC_TXT_LNE"].text = string.format( "%.3d", stpl - 1 ) --for read number of line
  end
end

--activate_tnc (check_timer, ENABLED/DISABLED)
TNC_CB = vb:checkbox { id = 'TNC_CB', value = false, notifier = function() tnc_check_pattern_first_line() line_timer() end, tooltip = "Enable/Disable TNC.\n" } --checkbox hidden
---
function tnc_cb_bt_ntf( song )
  song = renoise.song()
  if vb.views['TNC_CB'].value == false then
     vb.views['TNC_CB'].value = true
     vb.views['TNC_CB_BT'].color = { 0x00,0x70,0x00 }
     vb.views['TNC_CB_BT'].text = 'ENABLED'
     --- ---
     vb.views['TNC_TXT_TR'].text = song.tracks[song.selected_track_index].name
     vb.views['TNC_TXT_TR'].color = song.tracks[song.selected_track_index].color
    else
     vb.views['TNC_CB'].value = false
     vb.views['TNC_CB_BT'].color = { 0x40,0x00,0x00 }
     vb.views['TNC_CB_BT'].text = 'DISABLED'
  end
end
---
TNC_CB_BT = vb:button {
  id = 'TNC_CB_BT',
  width = 74,
  height = 22,
  text = 'DISABLED',
  color = { 0x40,0x00,0x00 },
  notifier = function() tnc_cb_bt_ntf() end,
  tooltip = 'Enable/Disable TNC.\n'
}

local CurrentLine = -1
function tnc_check_playback_line( song, spi, stpl, stel )
  song = renoise.song()
  --print("pos.line: ", song.transport.playback_pos.line )
  ---
  if ( CurrentLine == song.transport.playback_pos.line ) then
    return --<-- Important for not repeat the same function unnecessarily. Run the "tnc_change_buttons()" function only once for each line.
  else
    CurrentLine = song.transport.playback_pos.line
    --print("pos.line: ", song.transport.playback_pos.line )
    tnc_change_buttons()
  end

  --[[

  if (CurrentLine == song.transport.edit_pos.line) then
    return
  else
    CurrentLine = song.transport.edit_pos.line
    print("x") 
  end
  
  ]]
  ---
end

function line_timer()
  if vb.views['TNC_CB'].value == true then
    if not renoise.tool():has_timer( tnc_check_playback_line ) then
      renoise.tool():add_timer( tnc_check_playback_line, 5 ) --timer (ms) 5 = fast max, 20 = low, 40 = very low
    end
    --- ---
    --renoise.tool().app_new_document_observable:add_notifier( tnc_tr_chng )
    --renoise.tool().app_new_document_observable:add_notifier( tnc_tr_color )
    --renoise.tool().app_new_document_observable:add_notifier( tnc_check_pattern_first_line )
    --- ---
  else
    if renoise.tool():has_timer( tnc_check_playback_line ) then
      renoise.tool():remove_timer( tnc_check_playback_line )
    end
    --- ---
    
    --- ---
    --tnc_df() --default values
  end
end

How it works:

  1. An observable for the pattern is responsible for reading only the notes of the first line: tnc_check_pattern_first_line().
  2. Separately, a timer ( line_timer() ) is responsible for reading the rest of lines ( if not (song.selected_line_index == 1 ) then ), through the functiontnc_change_button().

Thus, point 1 changes the button properties only on line 1, point 2 changes the button properties of the rest of the lines throughout the song.Both functions include a switch (a checkbox to manually enable or disable).

I have used .is_emty also:

(1) if not ( song:pattern( spi ):track( sti ):line( 1 ).is_empty ) then

(2) for i = 1, 12 do ...
    
    if not ( song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).is_empty ) then

-------------

(1) if not ( song.selected_line.is_empty ) then

(2) for i = 1, 12 do
    
    if ( song.selected_line:note_column( i ).note_value < 120 ) then

With these lines inserted into the codeI guess it will free up more CPU processes.

To verify that the tool works well with the “timer (up to 5ms or more) + CPU used and speed (BPM and LPB)”, I do this (print with the terminal):

local CurrentLine = -1
function tnc_check_playback_line( song, spi, stpl, stel )
  song = renoise.song()
  print("pos.line: ", song.transport.playback_pos.line ) --<----- ACTIVATING THIS LINE
  ---
  if ( CurrentLine == song.transport.playback_pos.line ) then
    return --<-- Important for not repeat the same function unnecessarily. Run the "tnc_change_buttons()" function only once for each line.
  else
    CurrentLine = song.transport.playback_pos.line
    --print("pos.line: ", song.transport.playback_pos.line )
    tnc_change_buttons()
  end
end

When run the tool, looking the terminal, he prints the value of each line several times in sequence, then I can do tests by increasing the values of BPM and LPB.If a value (pos.line: X) is skipped, I understand that the tool may return an error, which may not coincide in time with this print, but there will probably be some error reading the data. Example:

Work ok! :

------------------------------------------(the terminal)

pos.line: 1

pos.line: 1

pos.line: 1

pos.line: 2

pos.line: 2

pos.line: 3

pos.line: 3

pos.line: 3

pos.line: 4

pos.line: 5

pos.line: 6

pos.line: 6

pos.line: 7

pos.line: 7

pos.line: 7


Will work with random errors:

------------------------------------------ (the terminal)

pos.line: 1

pos.line: 1

pos.line: 1

pos.line: 2

pos.line: 2

pos.line: 3

pos.line: 3

pos.line: 3

pos.line: 5 <<----(looking)------pos.line: 4 not exist!!! (the tool will probably return errors)

pos.line: 6

pos.line: 6

pos.line: 7

pos.line: 7

pos.line: 7


The second case demonstrates the lack of precision when using a fast timer:

  • Number 1 or 7 is repeated three times
  • Number 2 or 6 is repeated two times
  • Number 3 or 5 is repeated a time
  • Numeber 4 not exist! The tool will probably return random errors already.

Compiling everything, the whole situation is a sucks.

  1. To chase the line during the playback some kind of timer is needed…
  2. This timer will return errors at high speeds, at least > 200 BPM and > LPB.
  3. Under the hood of Renoise, using the timer reading the first line of each pattern returns errors, in 95% of cases.It is necessary to analyze the first line separately.The resulting code is not very elegant and mixes things with rare tricks.

Maybe Taktik could go deeper into this subject under the hood of Renoise to offer us a playback_pos.line _observable reliable enough to use at a reasonable speed, at least for those using powerful CPUs.Most related searches on the forums are from 2010/2011.It’s 2017.Have a playback_pos.line _observable would open the doors to a lot of interesting tools!

Danoise, you could discuss this with Taktik for an upcoming version of Renoise?

Everything is possible with the approaches given to you in this thread. I’ve tried to describe the problem of high LPM and how to avoid it earlier.

Simply put: If more than one line has passed, you can just ‘batch process’ the missed range with a loop.

PS. Regarding line observables… You certainly don’t need anything close to realtime for just controlling some button properties (the problem is not the API in this case).

Apart from all this stuff,I’ve thought of doing something else to avoid having 2 separate functions to read all the lines with following playback enabled.

The idea: using a function (A) with a pattern observable. Inside this function (A) include a timer (5 or 10 or 20 ms) which is activated at the beginning of each pattern, and stops at the end of each pattern.It would be the same timer for each pattern, but it will turn on and off at the beginning and end of each pattern.

I understand that the observable for the pattern, will ensure the correct reading of the first line, that is where there are problems.I do not know how it really works under the hood of Renoise maybe adding the timer this way, keeps returning errors in first line.I would have to try it…

Everything is possible with the approaches given to you in this thread. I’ve tried to describe the problem of high LPM and how to avoid it earlier.

Simply put: If more than one line has passed, you can just ‘batch process’ the missed range with a loop.

PS. Regarding line observables… You certainly don’t need anything close to realtime for just controlling some button properties (the problem is not the API in this case).

Thank Joule!

Now that I have tried using timer with a not very efficient approach, I should change method and look to build what you propose.

As I understand, I can use:

– Get a specific line range (index must be [1-Pattern.MAX_NUMBER_OF_LINES])

renoise.song().patterns[].tracks[]:lines_in_range(index_from, index_to)

→ [array of renoise.PatternLine objects]

– Serialize a line.

tostring (PatternLine object)

→ [string]

Maybe a function with idle_observable for check the pattern or a _observable for check the pattern ( .selected_pattern_index_observable:add_notifier(func) ), and get the line inside a specific range of lines.Can this return the line when follow playback is enabled? With follow playback the selected_line_index constantly changes…

@Raul: he’s not suggesting a fundamentally different approach. Simply that the script (when called through the timer) picks up the previous and current position and compares those. As mentioned earlier, it will guard you against those missed lines.
And you really need to plan for worst case scenarios. Believe me, a missed line here and there is nothing. Try to load a heavy plugin into the song and see how that affects your timers? Most likely, your script will start missing whole beats instead of just a line here and there. So, since you can’t prevent the timers from suddenly stopping, you can at least make sure your script can deal with this.

Maybe a function with idle_observable for check the pattern or a _observable for check the pattern ( .selected_pattern_index_observable:add_notifier(func) ), and get the line inside a specific range of lines.Can this return the line when follow playback is enabled? With follow playback the selected_line_index constantly changes…

Yes, this is good idea when the “follow player” mode is enabled. You will most likely receive those notifications (that the pattern has changed) before the idle notifier is able to pick up this change. I’m using this trick myself for realtime-critical tools.
But you don’t need to handle it in any special way. Just point to the same function that the idle one is calling - they are basically doing the same thing: responding to changes in the playback position.

@Raul: he’s not suggesting a fundamentally different approach. Simply that the script (when called through the timer) picks up the previous and current position and compares those. As mentioned earlier, it will guard you against those missed lines.
And you really need to plan for worst case scenarios. Believe me, a missed line here and there is nothing. Try to load a heavy plugin into the song and see how that affects your timers? Most likely, your script will start missing whole beats instead of just a line here and there. So, since you can’t prevent the timers from suddenly stopping, you can at least make sure your script can deal with this.

Yes, this is good idea when the “follow player” mode is enabled. You will most likely receive those notifications (that the pattern has changed) before the idle notifier is able to pick up this change. I’m using this trick myself for realtime-critical tools.
But you don’t need to handle it in any special way. Just point to the same function that the idle one is calling - they are basically doing the same thing: responding to changes in the playback position.

Really, I do not know how to add a “line tester”. I start from a function (F1) that works with a timer (T1).This function (F1) only returns errors on the first line, practically in all the passes (every time a new pattern starts). Adding a “line tester” will also depend on the timer (T1). Then, I do not know how to approach it to avoid errors, since it is inside a timer (T1) that is the cause of such errors. Maybe the function F1 that depends on the timer T1reads too fast the first line in the pattern change process. That’s why I add a patch (a function F2 that takes care of this line, the first).

I’ve almost finished the tool, and I’m pleasantly surprised by how well it works.I’ve tested with an Intel i7, up to BPM 700 and LPB 8, and seems to work fine.The faster you seem to run into the limits of the timer, which is set to 5ms.At first, it seemed that the tool failed more, but it was the cause of reading the first line.Now, the “patched” tool reads the first line with a pattern observable (O1) using the function F2, and the rest of the lines with the timer T1 with the función F1, without using :lines_of_range.I understand that these functions only analyze the selected pattern in question, using the timer. Are there any benefits to using lines_of_range inside the function F1 when running timer T1?It is necessary to analyze all the lines of the pattern, not a lower range.

Or… :lines_of_range is better than if .selected_line_index > 1 then?

Another issue is if there is any way to put the following to delay the reading of the first line a little: first use an “if”, and then an “elseif” to ensure that the first line is within the selected pattern to read, not the previous one.I’ll have to think about it.

On the other hand, I have not yet tested the tool on a laptop with an i3 CPU, much slower. But I get the feeling that it will work very fast too.Activating or deactivating the tool does not even involve a 0.1% increase in CPU load.After all, you are only modifying button properties and a bit of reading by timer.

This tool will be included inside GT16-Colors.Maybe I put a limiter with a warning window, to stop the tool if the BPM and LPB are very fast (for example BPM 500 and LPB 8 or equivalent).At least to me, I find it a bit exaggerated to compose with such a high pattern resolution.I find this somewhat unnecessary.But hey, the programmer has to make all the functions also in the most extreme cases.

I enclose a copy, in case yourselves want to have a look at the code and suggest improvements and changes:7391 tool_09.lua

Raul, have you thought that you are maybe thinking of the solution to ‘the first line on pattern change returns false note info’ a little complicated? Also, if you just take a moment to do a spot of math you can get a rough idea of how fast the timer needs to be. So for 120BPM at LPB 4 I would say a timer just less than 125ms. For say 400BPM at LPB 8 just a touch less than 18.75ms. BPM 700 at LPB 8, just a little less than 10.71ms. As that is possibly the speed roughly in which each line is flying past in the transport anyway.

But going back to the ‘pattern change first line problem’. The thing I would try is in the timer, detect when you are on the last line of the pattern, read in not only the last line note but also the first line of the next pattern in sequence (store that data somewhere.) Return last line note. Then (on another timer callback that comes around) detect when you have arrived at the first line of the next pattern, but instead of asking Renoise for the note info (because we know that can be erroneous), return what you have in your stored variable for that line. More complicated, not sure it would work, but that is what I would be thinking :slight_smile:

Raul, have you thought that you are maybe thinking of the solution to ‘the first line on pattern change returns false note info’ a little complicated? Also, if you just take a moment to do a spot of math you can get a rough idea of how fast the timer needs to be. So for 120BPM at LPB 4 I would say a timer just less than 125ms. For say 400BPM at LPB 8 just a touch less than 18.75ms. BPM 700 at LPB 8, just a little less than 10.71ms. As that is possibly the speed roughly in which each line is flying past in the transport anyway.

Ok, I had thought it out quickly, I could also add a timer updater in ms, using:

  1. BPM = renoise.song().transport.bpm, _observable → [number, 32-999]
  2. LPB = renoise.song().transport.lpb, _observable → [number, 1-256]
  3. The mathematical reference: speed (ms) = 60.000ms / ( BPM x LPB )… one minute has 60.000 ms
  4. & the timer:renoise.tool():add_timer( func, speed in ms )
  5. Maybe??? a 20% safety multiplier, x1.2:speed (ms) = 1.2 x 60.000ms / ( BPM x LPB )

Example: speed (ms) = 60.000ms / ( 120 x 4 ) = 125 ms

Another reference for information: LPS, (number of Lines Per Second) = (BPM x LPB) / 60 seg … example: (120 x 4) / 60s = 8 lines / second

…Maybe I use this to get closer to a sync.

But going back to the ‘pattern change first line problem’. The thing I would try is in the timer, detect when you are on the last line of the pattern, read in not only the last line note but also the first line of the next pattern in sequence (store that data somewhere.) Return last line note. Then (on another timer callback that comes around) detect when you have arrived at the first line of the next pattern, but instead of asking Renoise for the note info (because we know that can be erroneous), return what you have in your stored variable for that line. More complicated, not sure it would work, but that is what I would be thinking :slight_smile:

I’m not sure. If you check the next first line of the next pattern depends on the timer, it will return an error equally.I think it would not work. If you use a T1 timer to analyze all the first lines of all the patterns in a single instant, all the erroneous lines will be returned. I believe.Maybe there is a simple way to delay reading the first line, saying, do this after …

In any case implies adding something more special code for the first line.It’s a special case. Luckily, the API is quite flexible to find patches or to do a function in different ways.But what happened to the first line was a complete surprise to me :huh:.But everything seems to have a reasonable explanation, and therefore a solution.

Unfortunately, real synchronization is not easy between Renoise and tools …

By the way, I’d like to know how to pull concrete data from tostring:

– Serialize a line.

tostring(PatternLine object)

→ [string]

It seems something very isolated in the documentation, but I find it very interesting.By the way, if you use tostring within the timer, it will erroneously return the first line too.

A _observable is delayed or works in real time? or is similar to app_idle_observable ( 100ms or 150BPM & 4LPB ) ???

A _observable is delayed or works in real time? or is similar to app_idle_observable ( 100ms or 150BPM & 4LPB ) ???

Everything other than app_idle_observable and timers is very “sequential” and fully predictable.

At least in 99.99% of all cases. I extracted info about the sandbox in the formula device via parameter value observables, and remember i had to make the process slower to catch all characters. That was a very special case, though :slight_smile:

Regarding line strings, you can have a look at my revision of ledger’s “Set track width to active columns” - https://forum.renoise.com/t/new-tool-2-7-3-1-set-track-width-to-active-columns/31078

It uses pattern matching on line strings, which can be a lot faster than having to iterate note columns.

I’m not sure. If you check the next first line of the next pattern depends on the timer, it will return an error equally. I think it would not work. If you use a T1 timer to analyze all the first lines of all the patterns in a single instant, all the erroneous lines will be returned. I believe.

Ok, but I’m very biased to disagree with you Raul at the mo. I think you can read the first line of the next pattern (100% correctly) from the timer callback, so long as you don’t read the first line while Renoise is actually in the process of switching into the new pattern. Just annoying that you would now have to write code that follows the playing transport correctly (i.e. when I’m say on the last line of a pattern I’ve got to calculate the next sequence pattern (if there is one, if there isn’t, say read the first line from the first pattern? Or even where the user has specified a pattern loop in the sequencer?) and extract that first line to buffer it up before Renoise switches into the next pattern) and you would have to update your GUI button system to reflect this.

@Joule, Thanks!

A little more optimization:

--IMPORTANT NOTE, Renoise v3.1 not read correctly the fist line of each pattern using a timer or idle_obserble with playing the song. Is necessary use a filter for line 01.

--check first line only of each pattern (filter: tnc_check_pattern_first_line)
function tnc_check_pattern_first_line( song, spi, sti )
  song = renoise.song()
  spi, sti = song.selected_pattern_index, song.selected_track_index
  if ( song.selected_track.type == renoise.Track.TRACK_TYPE_SEQUENCER and song.transport.playing ) then -- only if song is playing
    if song.selected_pattern_index == spi then
      --print( "note line 01", song:pattern( spi ):track( sti ):line( 1 ):note_column( 6 ).note_string )
      for i = 1, 12 do
        if not ( song:pattern( spi ):track( sti ):line( 1 ).is_empty ) then --line 01 empty
          if not ( song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).is_empty ) then --column empty
            if ( song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).note_value ~= 120 ) then -- 120 = OFF, 121 = empty
              vb.views["TNC_NC_"..i..""].text = song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).note_string
              vb.views["TNC_NC_"..i..""].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
            else
              vb.views["TNC_NC_"..i..""].text = 'OFF'
              vb.views["TNC_NC_"..i..""].color = { 0x40,0x00,0x00 }
            end
          end
        end
        vb.views["TN_NC_NM_"..i..""].text = string.format(" %s", song:pattern( spi ):track( sti ):line( 1 ):note_column( i ).note_string )
      end
    end
  end
end

function tnc_first_line( song )
  song = renoise.song()
  if vb.views['TNC_CB'].value == true then
    if not ( song.selected_pattern_index_observable:has_notifier( tnc_check_pattern_first_line ) ) then
      song.selected_pattern_index_observable:add_notifier( tnc_check_pattern_first_line ) --observable for pattern
    end
  else
    if ( song.selected_pattern_index_observable:has_notifier( tnc_check_pattern_first_line ) ) then
      song.selected_pattern_index_observable:remove_notifier( tnc_check_pattern_first_line ) --observable for pattern
    end    
  end
end

--- ---

--tnc_change_buttons ( with timer: tnc_line_timer() ). In playing song, read all the lines except the first. Not playing song, read all the lines.
function tnc_change_buttons( song, spi, sti, stpl )
  song = renoise.song()
  spi, sti, stpl = song.selected_pattern_index, song.selected_track_index, song.transport.playback_pos.line
  if song.selected_track.type == renoise.Track.TRACK_TYPE_SEQUENCER then
    -- note column i, all lines > 1
    for i = 1, 12 do
      if song.transport.playing then
        if not (song.selected_line_index == 1 ) then
          if not ( song.selected_line.is_empty ) then --line > 01 empty
            if not ( song.selected_line:note_column( i ).is_empty ) then -- column empty
              if ( song.selected_line:note_column( i ).note_value ~= 120 ) then
                vb.views["TNC_NC_"..i..""].text = song.selected_line:note_column( i ).note_string
                vb.views["TNC_NC_"..i..""].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
              else
                vb.views["TNC_NC_"..i..""].text = 'OFF'
                vb.views["TNC_NC_"..i..""].color = { 0x40,0x00,0x00 }
              end
            end
          end
        end
      else -- to rescue line 01 with the song stopped.
        if not ( song.selected_line.is_empty ) then --line empty
          if not ( song.selected_line:note_column( i ).is_empty ) then -- column empty    
            if ( song.selected_line:note_column( i ).note_value ~= 120 ) then
              vb.views["TNC_NC_"..i..""].text = song.selected_line:note_column( i ).note_string
              vb.views["TNC_NC_"..i..""].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
            else
              vb.views["TNC_NC_"..i..""].text = 'OFF'
              vb.views["TNC_NC_"..i..""].color = { 0x40,0x00,0x00 }
            end
          end
        end
      end
      vb.views["TN_NC_NM_"..i..""].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string )
    end --for i
    vb.views["TNC_TXT_LNE"].text = string.format( "%.3d", stpl - 1 ) --for read number of line
  end
end

local CurrentLine = -1
function tnc_check_playback_line( song )
  song = renoise.song()
  if ( CurrentLine == song.transport.playback_pos.line ) then
    return --<-- Important for not repeat the same function unnecessarily. Run the "tnc_change_buttons()" function only once for each line.
  else
    CurrentLine = song.transport.playback_pos.line --(Run the "tnc_change_buttons()" function only once for each line)
    --print("pos.line: ", song.transport.playback_pos.line )
    tnc_change_buttons()
  end
end

function tnc_line_timer()
  if vb.views['TNC_CB'].value == true then
    if not renoise.tool():has_timer( tnc_check_playback_line ) then
      renoise.tool():add_timer( tnc_check_playback_line, 5 ) --timer (ms) 5 = fast max, 20 = low, 40 = very low
    end
  else
    if renoise.tool():has_timer( tnc_check_playback_line ) then
      renoise.tool():remove_timer( tnc_check_playback_line )
    end
  end
end

I think this improves the code above.I’ve followed some Danoise tips for if, and else looking for a more coherent order that saves unnecessary calls.

Ok, but I’m very biased to disagree with you Raul at the mo. I think you can read the first line of the next pattern (100% correctly) from the timer callback, so long as you don’t read the first line while Renoise is actually in the process of switching into the new pattern. Just annoying that you would now have to write code that follows the playing transport correctly (i.e. when I’m say on the last line of a pattern I’ve got to calculate the next sequence pattern (if there is one, if there isn’t, say read the first line from the first pattern? Or even where the user has specified a pattern loop in the sequencer?) and extract that first line to buffer it up before Renoise switches into the next pattern) and you would have to update your GUI button system to reflect this.

Sometimes I get carried away by theory, and then practice destroys it ^_^.If you are on the last line of pattern 01, and at this time you read line 01 of pattern 02, in theory at that moment you are not reading a new pattern, and therefore there will be no delay or error. You should store the data correctly. But this of the timers makes me dizzy.Probably works!!! :slight_smile:

Edit :If this were possible adding 6 or 8 more lines of code, it would involve drastically reducing my code.Directly, it would eliminate a function, and the principal function would be reduced in half.

Just as a side note I always found that your best question is still → https://forum.renoise.com/t/help-detect-son-member-inside-group-that-is-other-group-for-clone/47061

Checking this out.I think there is a simple way. Check if the selected track does not have note columns. If the track selected do not have note columns is not a Track, and if it is within a Group, it must be a Group.A Group can only contain Groups or Tracks ^_^.Confronting the cloning of whole groups is a challenge for me.Until Taktik does not update to the next version of Renoise (3.1.1), I should not try. The next version will have improvements related to a bug in the groups.

Related:https://forum.renoise.com/t/fixed-cloning-track-group-assigns-new-colors-other-things/47011

Checking this out. I think there is a simple way. Check if the selected track does not have note columns. If the track selected do not have note columns is not a Track, and if it is within a Group, it must be a Group.A Group can only contain Groups or Tracks ^_^.Confronting the cloning of whole groups is a challenge for me.Until Taktik does not update to the next version of Renoise (3.1.1), I should not try. The next version will have improvements related to a bug in the groups.

Hmm Raul, bold statements indeed :wink: I still say that is a very good question in a way. I admit that I only looked at it very briefly. The idea of a recursive function from Joule is probably sound (but tricky). However I found that before you even get to that to write a function that takes a top level group and copies all groups/tracks within the top level group and places it next to the group (i.e. you have completely cloned the top level group) is difficult with the functions you have on offer from the current API. Even Taktik (as you point out Raul) produces a bugged version of this in Renoise 3.1. Put it this way Raul, if Taktik doesn’t add anymore helper functions for when it comes to traversing the structure of the groups/tracks in Renoise (or even add like a clone group call), then I’m afraid you are in the same boat as you are now. Which means to answer that question would probably make this thread about ‘follow player position’ look like a small casual walk-in-the-park in simplicity in comparison to coming up with a stable and robust algorithm that answers that other thread :smiley: Not saying it isn’t possible (offhand) (I’m sure Danoise/Joule could come up with something), but I think that it would take some thought to write that function/code :slight_smile:

Check if the selected track does not have note columns. If the track selected do not have note columns is not a Track, and if it is within a Group, it must be a Group.

Don’t assume anything here - each track has a type you can compare against:

renoise.Track.TRACK_TYPE_SEQUENCER
renoise.Track.TRACK_TYPE_MASTER
renoise.Track.TRACK_TYPE_SEND
renoise.Track.TRACK_TYPE_GROUP

You’ll still have to wait for a fix for the bogged cloning of groups, though. This is obviously a bug in Renoise itself.

Don’t assume anything here - each track has a type you can compare against:

renoise.Track.TRACK_TYPE_SEQUENCER
renoise.Track.TRACK_TYPE_MASTER
renoise.Track.TRACK_TYPE_SEND
renoise.Track.TRACK_TYPE_GROUP

Yes, I do not remember exactly why I have trouble checking out child groups (groups within a group).I assumed to compare with renoise.Track.TRACK_TYPE_GROUP was sufficient, but I was not able.It would have to start again to know how to properly clone. Now I have this issue parked.

With regard to reading the first line of each pattern, I have finally chosen to rely on the observable of the pattern, by a separate function.The TNC tool has already been published as module inside the GT16-Colors tool , here:

https://forum.renoise.com/t/new-tool-3-1-gt16-colors-v1-2a1-updated-12-june-2017/46473

You’ll still have to wait for a fix for the bogged cloning of groups, though. This is obviously a bug in Renoise itself.

Yes, I’m waiting patiently.

Hello again!I put this topic as solved!

Thank you very much for all the help!

I would like to leave only one reflection.I think it would be a good thing if we had an effective line observable.To avoid having to resort to timers or other more complicated methods, which only a few people could build.

Finally, it seems that there are two ways to solve this issue:

  1. Using a timer that constantly bombards the update of each line.This is the method I have chosen, because the code is simpler.which is not 100% reliable at high speed.
  2. Use a kind of cache memory to read all the data of each pattern and project them in real time when necessary.I understand that it is more or less what Joule proposes.That cache must be updated every time the composer modifies anything in the pattern editor, within the range of the cache.I still do not understand this point. For this I would need to see a simple tool that works this way.

To create tools of this type, it is necessary to optimize the maximum code, as Danoise warns.Simply avoiding unnecessary calls increases the reaction speed to work “in real time” within the limits of the timer.

If the objective is to read data from particular note columns, it is better to create a function that directly returns that data without having to check empty pattern lines or empty track lines. The reason is that a timer is used. The more chained operations, the later it will return the data.Apparently, the shortest route to get the data is the fastest.That does not add conditions before.

A reliable tool would be one that works perfectly at a high speed, something like 900 BPM & 8 LPB as minimum without any error.With timer it is frankly difficult to achieve that.

Finally, being able to get reliable data from the line can accommodate a lot of useful tools:

  • Time reader, with resolution in tenths of a second, per line.A time marker could be built.
  • A visual metronome configurable. This is very interesting.

Converter of musical notes, even with a selector of clef.

Of course, an orderly note follower to find melody errors and to improve the melody (my tool).

A track player based on blocks per pattern.

Up to 3 track playersbased on blocks per pattern.The composer could select 3 tracks to compare.

Etc.

If there is no reliable way of reading data for each line, all these tools can return problems…

:blink:

Okay, I’ve been reviewing again what exactly happens here. And now it’s very clear.I have come to use a timer of 1ms (practically in real time), to trace the selected line constantly, not including the first line of each pattern.

The results have been excellent, without any kind of strange error. It is 100% clear that the jump from one pattern to another causes some alteration in the reading (perhaps a little delay) that randomly returns errors.

This means that if this small problem were solved in line one, we could use a timer of 1ms without any error in all the lines. So the problem is not the very small time of the timer, but what happens before the timer takes over.

Please, I formally request that this be solved that provokes errors when jumping between patterns. A _observable for selected_line_indexit could work almost instantly (1ms). But you must go very thin at the precise moment change of pattern.