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

As I understand from Danoise’s comments…

I have the current position, I can choose between edit_pos.line or playback_pos.line.And I have to compare this variable with a fixed value, which I do not know how to fix.

I tried to do comparison tests with “if x == y then” inside the function with timer activated.But since the function does not stop repeating, the “fixed value” also changes.Do I have to set a value within a function that works with a timer?I do not understand how I should pose what Danoise comments…

Edit: In this case, if following the player is not activated, in theory the final function would not run. It is not like this?

I’m not fully sure what you mean by ‘following the player’. But what I can say is that so long as that dialog (in my example) is on the screen (and you haven’t switched to another application, because that would probably also stop the timer), that timer function is called around every 20ms, irrespective of the transport started/stopped state. All this is Raul is you have a timer function ‘in the background’ sampling the pattern every 20ms. If your timer function overruns 20ms it will probably begin to stutter and skip etc… (believe me, not good). If you set your timer to say 5ms (to sample the pattern faster) make sure that the code in the timer function executes in less than 5ms of CPU time. You can’t just keep adding code to that timer function, at some point it could/will overrun. Now you say: ‘Well I’ll just set the timer to 40ms to give it more time’. Yup, you could do this, but now you are sampling the pattern at a lower frequency, so now you run the risk of missing notes at higher LPB/BPM. See part of the game here? Becomes more and more inaccurate.

The issue is that I do not know how to translate this, to handle a button and its properties, color, name, to be invoked only once per line.

So now we’ve moved on from sampling the pattern to the question: How do I work out if the line has changed? Well if you look at this section of code:

if (CurrentLine == rs.transport.edit_pos.line) then
    return
  else
    CurrentLine = rs.transport.edit_pos.line
  end

Is there not a return statement in there? If the CurrentLine is equal to the Renoise transport edit position then leave the timer function? Otherwise set the CurrentLine to the transport edit position. In other words anything that comes after that If/else statement, is only executed on a change of line.

if (CurrentLine == rs.transport.edit_pos.line) then
    return <<--------------------------------------- RETURN!!!
  else
    CurrentLine = rs.transport.edit_pos.line
    -- here launch the desired function
  end

Ok, this matter is solved already!!! :o :o :o

Thank you very much for the help, Danoise and 4Tey!!!

However, it does not solve a very strange reading problem. I explain…

You imagine a complex piano melody written using the 12 note columns, occupying several patterns.When the tool is working, it illuminates the buttons correctly (each button represents a note column).An image of support:

[sharedmedia=core:attachments:7374]

But a concrete failure always occurs:

When Renoise read the first line of each pattern (line 001 of each note column), if the line is empty, it sometimes "read the same note (change the characteristics of the button) as the first line of the previous pattern. It happens almost 90% of the time.It is a subtle error very annoying. Always the same. If I move the note of the previous pattern on line number 002, the error no longer occurs.

A ) Example of error :wacko::-------------------------------------------------------------------------

---------------- Pattern 01, note column 03

C-0 line 001

D-2 line 002

---------------- Pattern 02, note column 03

nocing line 001 <------- ERROR, here read a C-0 (As the same note from the previous pattern (same line 001)

… line 002

B ) Example not error-------------------------------------------------------------------

---------------- Pattern 01, note column 03

nocing line 001

D-2 line 002

---------------- Pattern 02, note column 03

nocing line 001 <------- NOT ERROR, because in the previous patter in line 001 is empty )

… line 002

This is the function that executes once each time a line starts:

function 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 1
      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
      -- note column 2
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 2 ).note_string ~= '---' then
        vb.views["TNC_NC_02"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 2 ).note_string
        vb.views["TNC_NC_02"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 2 ).note_string == 'OFF' then
        vb.views["TNC_NC_02"].color = { 0x40,0x00,0x00 }
      end
      -- note column 3
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 3 ).note_string ~= '---' then
        vb.views["TNC_NC_03"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 3 ).note_string
        vb.views["TNC_NC_03"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 3 ).note_string == 'OFF' then
        vb.views["TNC_NC_03"].color = { 0x40,0x00,0x00 }
      end
      -- note column 4
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 4 ).note_string ~= '---' then
        vb.views["TNC_NC_04"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 4 ).note_string
        vb.views["TNC_NC_04"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 4 ).note_string == 'OFF' then
        vb.views["TNC_NC_04"].color = { 0x40,0x00,0x00 }
      end
      -- note column 5
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 5 ).note_string ~= '---' then
        vb.views["TNC_NC_05"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 5 ).note_string
        vb.views["TNC_NC_05"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 5 ).note_string == 'OFF' then
        vb.views["TNC_NC_05"].color = { 0x40,0x00,0x00 }
      end
      -- note column 6
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 6 ).note_string ~= '---' then
        vb.views["TNC_NC_06"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 6 ).note_string
        vb.views["TNC_NC_06"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 6 ).note_string == 'OFF' then
        vb.views["TNC_NC_06"].color = { 0x40,0x00,0x00 }
      end
      -- note column 7
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 7 ).note_string ~= '---' then
        vb.views["TNC_NC_07"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 7 ).note_string
        vb.views["TNC_NC_07"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 7 ).note_string == 'OFF' then
        vb.views["TNC_NC_07"].color = { 0x40,0x00,0x00 }
      end
      -- note column 8
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 8 ).note_string ~= '---' then
        vb.views["TNC_NC_08"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 8 ).note_string
        vb.views["TNC_NC_08"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 8 ).note_string == 'OFF' then
        vb.views["TNC_NC_08"].color = { 0x40,0x00,0x00 }
      end
      -- note column 9
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 9 ).note_string ~= '---' then
        vb.views["TNC_NC_09"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 9 ).note_string
        vb.views["TNC_NC_09"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 9 ).note_string == 'OFF' then
        vb.views["TNC_NC_09"].color = { 0x40,0x00,0x00 }
      end
      -- note column 10
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 10 ).note_string ~= '---' then
        vb.views["TNC_NC_10"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 10 ).note_string
        vb.views["TNC_NC_10"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 10 ).note_string == 'OFF' then
        vb.views["TNC_NC_10"].color = { 0x40,0x00,0x00 }
      end
      -- note column 11
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 11 ).note_string ~= '---' then
        vb.views["TNC_NC_11"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 11 ).note_string
        vb.views["TNC_NC_11"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 11 ).note_string == 'OFF' then
        vb.views["TNC_NC_11"].color = { 0x40,0x00,0x00 }
      end
      -- note column 12
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 12 ).note_string ~= '---' then
        vb.views["TNC_NC_12"].text = song:pattern( spi ):track( sti ):line( stpl ):note_column( 12 ).note_string
        vb.views["TNC_NC_12"].color = tnc_tr_color()--{ 0x00,0x70,0x00 }
      end
      if song:pattern( spi ):track( sti ):line( stpl ):note_column( 12 ).note_string == 'OFF' then
        vb.views["TNC_NC_12"].color = { 0x40,0x00,0x00 }
      end
      --- ---
      vb.views['TN_NC_NM_01'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string )
      vb.views['TN_NC_NM_02'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 2 ).note_string )
      vb.views['TN_NC_NM_03'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 3 ).note_string )
      vb.views['TN_NC_NM_04'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 4 ).note_string )
      vb.views['TN_NC_NM_05'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 5 ).note_string )
      vb.views['TN_NC_NM_06'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 6 ).note_string )
      vb.views['TN_NC_NM_07'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 7 ).note_string )
      vb.views['TN_NC_NM_08'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 8 ).note_string )
      vb.views['TN_NC_NM_09'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 9 ).note_string )
      vb.views['TN_NC_NM_10'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 10 ).note_string )
      vb.views['TN_NC_NM_11'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 11 ).note_string )
      vb.views['TN_NC_NM_12'].text = string.format(" %s", song:pattern( spi ):track( sti ):line( stpl ):note_column( 12 ).note_string )
      --- --- 
  end 
end

It can probably be reduced.But, what can this error be?All notes already written on line 001 are well read, except the strange case above, which seems to read a note that is not written in the pattern editor…Really strange!!!

I’ll look to create a separate tool for them to you see…

Ok, here is the tool “finished”. Is named “Track Notes Checker”. Please test the tool for search errors! :

  1. The tool TNC “Track Notes Checker” :7378 ulneiz.TNC_v1.0b1.xrnx

Look at all the time that happens whenever you read the first line of each pattern (line 00, first line). Something does not work right here.Need to fix this little bug.Any idea of the cause of this error?

I have tried the tool even at a speed of: BPM = 400 aprox. and LPM = 8.That’s more than twice the speed I usually use.I would like to leave this perfect tool, within its limitations by the timer.

Why do you read a note on line 00 when it is not written?It is strange, because the notes that are already written on line 00 are read correctly.

To check, I think all notes have OFF at the end in the song for test…

The operation is very basic: if exist note or note off, change name and color for each button, (12 buttons, one for each note column).If you write complex melodies using the 12 note columns, following the order of one octave, it is very simple to visualize.Can be very fun!!! :slight_smile:

7380 ulneiz.TNC.png

Please prove it and help to perfect it!!!

Thank you very much for the help and the patience!!!

Note: The read is made to be continuous between patterns, let the song be a piece. You need to activate the follow player…

Look at all the time that happens whenever you read the first line of each pattern (line 00, first line). Something does not work right here.Need to fix this little bug.Any idea of the cause of this error?

I’ve had a quick look Raul. I’ll try and explain what I think the problem is here. When Renoise is playing and it changes pattern in the sequence, when you read the note data (sampled from your timer function) of the first line from the new pattern say using:

song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string

the note data you get back is probably incorrect. So let’s assume that there is a ‘setup’ time when Renoise switches patterns when playing, the timer catches it (sometimes) when it is in this ‘setup’ time and…you get incorrect note data back when you read the first line. I would say it is nothing to do with the logic in your code Raul.

Offhand (because Renoise has put a spanner in the works) the thing to try would be to say buffer up at least the next line (or a few lines ahead) in the pattern/sequence on each timer call, and read from that. Doable, but more tricky/messy to code though. Also remember that you don’t want overruns, so whatever buffering/caching system you come up with (including button/text rendering, because at the moment you have all of that gui changing stuff executed in your timer function) it has got to complete in less time than the time you set your periodic timer (which at the moment from your code is set at 5ms. I assume you are not planning on running this on a 33MHz 486 then :wink: )

I also took a quick look. Raul - I have to say, for a realtime script, you are actually being quite wasteful…

if song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string ~= '---' then
  -- do something
end
if song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string == 'OFF' then
  -- do something
end
-- etc...

There is nothing “wrong” here per se, but several things you can do better.
As per my third advice in the lua optimization thread, it’s good idea to store references to objects that you are going to call repeatedly.

First of all, it’s very simple to avoid several function calls by storing a reference to the line you’re working on:

local ln = song:pattern( spi ):track( sti ):line( stpl )

This way, you avoid the three API functions you are calling - pattern(), track() and line() - each time you need to access a note column.
And since you are using the line above a total of 48 times, this is 48*3 == 144 function calls avoided, with one simple change.

It makes the code a lot easier to read too :slight_smile:

Another improvement is to use the if … else syntax to simplify comparisons:

if (ln:note_column( 1 ).note_string ~= '---') then
-- do something
elseif (ln:note_column( 1 ).note_string == 'OFF') then
-- do something else
end

Or better still, using the previous technique and storing a reference to the note-column (additionalcalls avoided)

local col = ln:note_column( 1 )
if (col.note_string ~= '---') then
-- do something
elseif (col.note_string == 'OFF') then
-- do something else
end

Using if…else is more efficient, since the second statement - looking for the (“OFF”) - will never be evaluated if the first part was found to be true.

Now, with such a simple example there might not be a big impact. But imagine that your code was more complex than this? Skipping parts as early as possible can be a HUGE performance booster.

And both are general principles which can be applied to any programminglanguage :slight_smile:

Also remember that you don’t want overruns, so whatever buffering/caching system you come up with (including button/text rendering, because at the moment you have all of that gui changing stuff executed in your timer function) it has got to complete in less time than the time you set your periodic timer (which at the moment from your code is set at 5ms. I assume you are not planning on running this on a 33MHz 486 then :wink: )

I never used the timer, so I don’t have much experience with that.To me, idle_notifier is the natural choice, as it simply runs when Renoise has time for it.

I never used the timer, so I don’t have much experience with that.To me, idle_notifier is the natural choice, as it simply runs when Renoise has time for it.

I prefer the timer notifier system myself, but if you are more comfortable with doing it in the idle notifier danoise (when you have a dynamic time frame callback), that is fair enough :slight_smile: The reason I tend to prefer the timer system is that I always get the feeling I am more guaranteed to get a callback in my set time frame even when Renoise is say playing a song and is under more pressure. But it probably doesn’t really make much difference here which ever way you go danoise. The point is we want a guaranteed callback that happens at least between 16-20ms (even when Renoise is playing a song). But you have got to read in slightly more lines ahead of the transport (making a subtle cache of lines) on each timer/idle callback because as Raul as shown the pattern change causes a ‘glitch’ in extracting the note value/string on the first line of the new pattern if you try and read just the one line per callback. As for optimizing lua, go bananas. Optimize as much as possible in the idle/timer notifier as you specified above danoise/Joule :slight_smile:

I also took a quick look. Raul - I have to say, for a realtime script, you are actually being quite wasteful…

if song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string ~= '---' then
-- do something
end
if song:pattern( spi ):track( sti ):line( stpl ):note_column( 1 ).note_string == 'OFF' then
-- do something
end
-- etc...

There is nothing “wrong” here per se, but several things you can do better.
As per my third advice in the lua optimization thread, it’s good idea to store references to objects that you are going to call repeatedly.

First of all, it’s very simple to avoid several function calls by storing a reference to the line you’re working on:

local ln = song:pattern( spi ):track( sti ):line( stpl )

This way, you avoid the three API functions you are calling - pattern(), track() and line() - each time you need to access a note column.
And since you are using the line above a total of 48 times, this is 48*3 == 144 function calls avoided, with one simple change.

It makes the code a lot easier to read too :slight_smile:

Another improvement is to use the if … else syntax to simplify comparisons:

if (ln:note_column( 1 ).note_string ~= '---') then
-- do something
elseif (ln:note_column( 1 ).note_string == 'OFF') then
-- do something else
end

Or better still, using the previous technique and storing a reference to the note-column (additionalcalls avoided)

local col = ln:note_column( 1 )
if (col.note_string ~= '---') then
-- do something
elseif (col.note_string == 'OFF') then
-- do something else
end

Using if…else is more efficient, since the second statement - looking for the (“OFF”) - will never be evaluated if the first part was found to be true.

Now, with such a simple example there might not be a big impact. But imagine that your code was more complex than this? Skipping parts as early as possible can be a HUGE performance booster.

And both are general principles which can be applied to any programminglanguage :slight_smile:

I never used the timer, so I don’t have much experience with that.To me, idle_notifier is the natural choice, as it simply runs when Renoise has time for it.

I explain a little my current way of making code.

  1. My first objective is to test if something works.If it works, it does not matter whether the code is heavier or not, it just works.
  2. Point 1 forces me to study their parts, because it allows me to see what I can do or not with them (If I do the “compressed code”, I do not see some parts).This usually happens to me because at first I do not know if I will be able to build a concrete code. I am in the testing phase, and trial and error.
  3. After checking if the code works, I start to optimize it.I have not been with LUA for a long time, but I recognize that I have undergone an evolution, and more with the Renoise API.

Last night, I did an optimization phase using these two things:

--change_buttons
function change_buttons( song, spi, sti, stpl, i )
  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

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

I’m calling 12 equal buttons, so I can use “for … do”.I could also use a “local” to save calls for:

song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string

Other Optimization:

--default values
function tnc_df( tx, cl, i )
  tx = '---'
  cl = { 0x20,0x00,0x00 }
  for i = 1, 12 do
    vb.views["TNC_NC_"..i..""].text = tx
    vb.views["TNC_NC_"..i..""].color = cl
  end
end

Other, if use song.selected_line instead of song.transport.playback_pos.line ( =stpl ) inside the

song:pattern( spi ):track( sti ):line( stpl ):note_column( i ).note_string

I can use:

song.selected_line:note_column( i ).note_string

This is in phase of code optimization. But the fact is that both the previous code and the “compressed code”, work the same, they are asking for the same thing.I have tried both situations and Renoise works exactly the same.If Renoise returns “a error”, it will continue to return, maybe less, but there it is.The subject is how to skip it.

I have read the last comment of 4Tey, and seems to read my mind.It seems that in the pattern jump to the next pattern, the request is faster perhaps in analyzing the line than in changing pattern, thus written errors of notes appear, maybe.The code seems logical and correct, only that does not prevent Renoise’s erratic behavior.

Based on theory alone, it should work perfectly just the way it is.Now I have no idea how to skip this subtle error!!!

Regarding using idle_notifier or a timer, the idle_notifier work about 100ms aprox. (I have understood). Then it is slow to read the update of each line.It happens that the duration of a single line is less than those 100 ms, even to a not very fast reproduction (below BPM 300 and LPB 8).

So I prefer to use a timer.But if the fast timer returns errors, I think it does not depend on the timer itself, but on the code under the hood of Renoise.

So now I’m dead point.How can I avoid these errors, if the code is apparently correct.

Another thing.At least to change things from the viewbuilder,I think that an observable fortransport.playback_pos.line and selected_line / selected_line_index would be very feasible.I wish it were possible.If the speed is very high, it might be appropriate to put an internal limiter to the observable, and at slower speed, that works in real time, to avoid having to invent code to solve such simple things.With a notice to the programmer in the documentation of the risks of using an observable in real time to follow the line or the playback, would be solved.I can think of a lot of useful tools thanks to this. Would be wonderful!!!

When I have a little time,I will share a v1.0b2 of the tool to see if we can find a way to make it work perfectly.It would be a good example recently. Most old tools that are related to following the line at all times, do not workor are outdated (not supported Renoise 3.1).

Thanks for all the help, lately I’m learning those more advanced tricks that allow me to think differently.

  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.

  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.