Save table to a TXT file, load table from this TXT file.


(Raul (ulneiz)) #1

I’m investigating how to get this: export a tabl inside a new TXT file, to have it saved for later sessions (one table for each TXT file, renamed differently).

In this way, days later it is possible to access this TXT to return the saved table and use it again in the tool, as if it were a bank.

Does anyone have a way of doing all this with a simple code? No extra-long functions to convert a table into a chain or things like that.The tables would not be much. They would contain at most 128 values.

I’m experimenting with the following code:

--
-- test area
--

function kng_write_txt( file, tbl )
    print( "------------------------ " )
    print( "write in "..file )
    io.output( io.open( file, "w" ) )
    io.write( tbl )
    io.close()
    print( "---- write done ----" )
end

function kng_read_txt(file)
    print( "------------------------ " )
    print( "read from "..file )
    io.input( io.open( file, "r" ) )
    local li
    for i in io.lines() do print(i) end
    -- no close needed: done by io.lines()
    print("---- read done ----")
end

function kng_test()
  local dir = os.currentdir() --dir for tool
  local txt = "txt/table_1.txt"
  local file = dir..txt

  local tbl =" { 1,2,3,4,5,6,7,8,9,10 } " --?????????????
  --local t = table.ToString(tbl) ?????????????
  --print(t)
  kng_write_txt( file, tbl )
  --kng_read_txt( file )
end

KNG_TEST = vb:button {
  text = "Test",
  notifier = function() kng_test() end
}

At the moment, I have problems converting a table into a string.Possibly, it is convenient not to include the keys “{” “}” , only the values. Is there nothing in LUA that allows this? Is it necessary to create a function that iterates between all the values to create the string? Subsequently, this string must be used to import the values again into the tool to create another table and load it.

Any simple suggestion?


(ffx) #2

Read about JSON.


(joule) #3

The very quick and easy way is to use the Document class. It can be implemented in various ways, but here is a really simple way that’s working:

I find that the Document XML import/export works really well, and don’t really get the old talk about using other means of serialization/deserialization.

-- Create and define a Document the quick and simple way - from a "model" table.
-- Note that the Document lists are picky about differentiating between numberlists and string lists.
my_document = renoise.Document.create("MyDataID") {
  number_list = { 1, 2, 3 },
  string_list = { "1", "some_string", "3" },
}

-- Loads data from test.xml, if the file exists
my_document:load_from("test.xml")

oprint(my_document:property("string_list")[2].value)

my_document:property("string_list")[2].value = "Wow .. new data"

-- Save any changed data to test.xml
my_document:save_as("test.xml")

(Raul (ulneiz)) #4

The very quick and easy way is to use the Document class. It can be implemented in various ways, but here is a really simple way that’s working:

I find that the Document XML import/export works really well, and don’t really get the old talk about using other means of serialization/deserialization.

-- Create and define a Document the quick and simple way - from a "model" table.
-- Note that the Document lists are picky about differentiating between numberlists and string lists.
my_document = renoise.Document.create("MyDataID") {
number_list = { 1, 2, 3 },
string_list = { "1", "some_string", "3" },
}

-- Loads data from test.xml, if the file exists
my_document:load_from("test.xml")

oprint(my_document:property("string_list")[2].value)

my_document:property("string_list")[2].value = "Wow .. new data"

-- Save any changed data to test.xml
my_document:save_as("test.xml")

Then I will insist on using the XML format. I have already managed to successfully implement it to save XML banks in separate files. It will even allow you to check the mother folder to avoid errors.

os.mkdir(path) and**io.exists(filename)**work correctly in all operating systems?The documentation does not say anything about compatibility, but I understand that it should work on any operating system compatible with Renonise.If so, it is very easy to create and verify a directory to be able to save several XML files.

I attach the code that I created for the creation of XML data banks. If someone sees any improvement, you just have to mention it. From what I’ve tried, it works perfectly.

Click to view contents
--
-- banks panel
--

class "Kng_Bank"
function Kng_Bank:__init( val )
  self.cnt = vb:row { spacing = 3,
    vb:text {
      height = 21,
      width = 21,
      align = "right",
      text = ("%.2d"):format( val )
    },
    vb:textfield {
      id = "KNG_BANK_TXF_"..val,
      height = 21,
      width = 137,
      text = ("Bank %.2d"):format( val ),
      tooltip = ("Rename the bank %.2d"):format( val )
    },
    vb:row {
      vb:row { spacing = -3,
        vb:button {
          id = "KNG_BANK_BT_LOCK_SAVE_"..val,
          height = 21,
          width = 25,
          bitmap = "/ico/mini_padlock_close_ico.png",
          notifier = function() kng_lock_save_bank( val ) end,
          tooltip = ("Lock save the bank %.2d"):format( val )
        },        
        vb:button {
          active = false,
          id = "KNG_BANK_BT_SAVE_"..val,
          height = 21,
          width = 35,
          bitmap = "/ico/save_ico.png",
          notifier = function() kng_save_bank( val ) end,
          tooltip = ("Save the bank %.2d. Unlock before!"):format( val )
        }
      },
      vb:button {
        id = "KNG_BANK_BT_LOAD_"..val,
        active = false,
        height = 21,
        width = 65,
        text = ("Load %.2d"):format( val ),
        notifier = function() kng_load_bank( val ) end,
        midi_mapping = ("Tools:KangarooX120:Banks:Load %.2d"):format( val ),
        tooltip = ("Load the bank %.2d"):format( val )
      }
    }
  }
end
---
function kng_bank( val_1, val_2 )
  local tbl = { 4,12,20,28,36,44,52,60,68,76,84,92 }
  local bank = vb:column {}
  for num = val_1, val_2 do
    bank:add_child (
      Kng_Bank( num ).cnt
    )
    if table.find( tbl, num, 1 ) ~= nil then
      bank:add_child (
        vb:space { height = 5 }
      )
    end
  end
  return bank
end
---
KNG_BANKS_1 = vb:row {
  kng_bank( 1, 8 ),
  vb:space { width = 6 },
  kng_bank( 9, 16 ),
  vb:space { width = 6 },
  kng_bank( 17, 24 )
}
---
KNG_BANKS_2 = vb:row {
  visible = false,
  kng_bank( 25, 32 ),
  vb:space { width = 6 },
  kng_bank( 33, 40 ),
  vb:space { width = 6 },
  kng_bank( 41, 48 )
}
---
KNG_BANKS_3 = vb:row {
  visible = false,
  kng_bank( 49, 56 ),
  vb:space { width = 6 },
  kng_bank( 57, 64 ),
  vb:space { width = 6 },
  kng_bank( 65, 72 )
}
---
KNG_BANKS_4 = vb:row {
  visible = false,
  kng_bank( 73, 80 ),
  vb:space { width = 6 },
  kng_bank( 81, 88 ),
  vb:space { width = 6 },
  kng_bank( 89, 96 )
}
---
function kng_banks_sel( val )
  if ( val == 1 ) then
    KNG_BANKS_1.visible = true
    KNG_BANKS_2.visible = false
    KNG_BANKS_3.visible = false
    KNG_BANKS_4.visible = false
    vws.KNG_BANKS_SEL_1.color = KNG_CLR.WHITE
    vws.KNG_BANKS_SEL_2.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_3.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_4.color = KNG_CLR.DEFAULT
  elseif ( val == 2 ) then
    KNG_BANKS_1.visible = false
    KNG_BANKS_2.visible = true
    KNG_BANKS_3.visible = false
    KNG_BANKS_4.visible = false
    vws.KNG_BANKS_SEL_1.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_2.color = KNG_CLR.WHITE
    vws.KNG_BANKS_SEL_3.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_4.color = KNG_CLR.DEFAULT
  elseif ( val == 3 ) then
    KNG_BANKS_1.visible = false
    KNG_BANKS_2.visible = false
    KNG_BANKS_3.visible = true
    KNG_BANKS_4.visible = false
    vws.KNG_BANKS_SEL_1.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_2.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_3.color = KNG_CLR.WHITE
    vws.KNG_BANKS_SEL_4.color = KNG_CLR.DEFAULT
  else
    KNG_BANKS_1.visible = false
    KNG_BANKS_2.visible = false
    KNG_BANKS_3.visible = false
    KNG_BANKS_4.visible = true
    vws.KNG_BANKS_SEL_1.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_2.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_3.color = KNG_CLR.DEFAULT
    vws.KNG_BANKS_SEL_4.color = KNG_CLR.WHITE
  end
end
---
KNG_BANKS_SEL = vb:column { spacing = -3,
  vb:space { height = 5 },
  vb:bitmap {
    height = 27,
    width = 56,
    mode = "body_color",
    bitmap = "/ico/bank_ico.png"
  },
  vb:text {
    height = 19,
    width = 56,
    align = "center",
    font = "big",
    text = "Banks"
  },
  vb:space { height = 7 },
  vb:button {
    id = "KNG_BANKS_SEL_1",
    height = 34,
    width = 56,
    color = KNG_CLR.WHITE,
    text = "01 - 24",
    notifier = function() kng_banks_sel( 1 ) end,
    midi_mapping = "Tools:KangarooX120:Banks:Show Banks 01-24"
  },
  vb:button {
    id = "KNG_BANKS_SEL_2",
    height = 34,
    width = 56,
    text = "25 - 48",
    notifier = function() kng_banks_sel( 2 ) end,
    midi_mapping = "Tools:KangarooX120:Banks:Show Banks 25-48"
  },
  vb:button {
    id = "KNG_BANKS_SEL_3",
    height = 34,
    width = 56,
    text = "49 - 72",
    notifier = function() kng_banks_sel( 3 ) end,
    midi_mapping = "Tools:KangarooX120:Banks:Show Banks 49-72"
  },
  vb:button {
    id = "KNG_BANKS_SEL_4",
    height = 34,
    width = 56,
    text = "73 - 96",
    notifier = function() kng_banks_sel( 4 ) end,
    midi_mapping = "Tools:KangarooX120:Banks:Show Banks 73-96"
  }
}
---
KNG_BANKS = vb:row { margin = 1,
  visible = false,
  vb:row { margin = 5, style = "panel",
    vb:column {
      KNG_BANKS_1,
      KNG_BANKS_2,
      KNG_BANKS_3,
      KNG_BANKS_4
    },
    vb:space { width = 12 },
    KNG_BANKS_SEL
  }
}

--lock save bank
KNG_BANK_LOCK_SAVE = { --96
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true,
  true, true, true, true, true, true, true, true
} 
function kng_lock_save_bank( val )
  if ( KNG_BANK_LOCK_SAVE[val] == true ) then
    vws["KNG_BANK_BT_SAVE_"..val].active = true
    vws["KNG_BANK_BT_LOCK_SAVE_"..val].bitmap = "/ico/mini_padlock_open_ico.png"
    KNG_BANK_LOCK_SAVE[val] = false
  else
    vws["KNG_BANK_BT_SAVE_"..val].active = false
    vws["KNG_BANK_BT_LOCK_SAVE_"..val].bitmap = "/ico/mini_padlock_close_ico.png"
    KNG_BANK_LOCK_SAVE[val] = true
  end
end

--save bank
function kng_save_bank( val )
  --print (io.exists("banks"))
  --check folder "banks", if it does not exist, create it
  if ( io.exists("banks") == false ) then
    --create new directory "banks"
    os.mkdir("banks")
    rna:show_status( "KangarooX120: The \"banks\" folder has been restored!" )
  end
  --create doc with pad data
  local doc = renoise.Document.create("Kng_Bank_"..val.."") {
    state = true,
    nme = vws["KNG_BANK_TXF_"..val].text,
    nte = KNG_PAD_NTE,
    ins = KNG_PAD_INS,
    trk = KNG_PAD_TRK,
    vel = KNG_PAD_VEL,
    clr = KNG_PAD_CLR.value
  }
  --save the doc in xml
  doc:save_as("banks/bank_"..val..".xml")
  kng_revise_bank( 1, 96 )
  rna:show_status( ("KangarooX120: Bank %.2d saved! The 96 Banks have been revised again!"):format(val) )
  
  --lock
  vws["KNG_BANK_BT_SAVE_"..val].active = false
  vws["KNG_BANK_BT_LOCK_SAVE_"..val].bitmap = "/ico/mini_padlock_close_ico.png"
  KNG_BANK_LOCK_SAVE[val] = true
  
end

--load bank
function kng_load_bank( val )
  --check folder "banks"
  if ( io.exists("banks") == true ) then
    --create neutral doc ( it is necessary for invoke after the doc:load_from() ), and not save!
    local doc = renoise.Document.create("Kng_Bank_"..val.."") {
      state = false,
      nme = "",
      nte = { 0 },
      ins = { 0 },
      trk = { 0 },
      vel = { 0 },
      clr = 1
    }
    --load doc to restore
    doc:load_from("banks/bank_"..val..".xml")
    --oprint(doc:property("state").value)
    
    --import data to pad from xml
    if ( doc:property("state").value == true ) then
      for i = 1, 120 do
        KNG_PAD_NTE[i] = doc:property("nte")[i].value
        KNG_PAD_INS[i] = doc:property("ins")[i].value
        KNG_PAD_TRK[i] = doc:property("trk")[i].value
        KNG_PAD_VEL[i] = doc:property("vel")[i].value
        vws["KNG_PAD_"..i - 1].text = ("%.2d\n%s %.2d\nTr%.2d"):format( i, kng_note_tostring( KNG_PAD_NTE[i] ), KNG_PAD_INS[i], KNG_PAD_TRK[i] )
        vws["KNG_PAD_ROT_VEL_"..i - 1].value = KNG_PAD_VEL[i]
      end
      KNG_PAD_CLR.value = doc:property("clr").value
      rna:show_status( ("KangarooX120: Bank %.2d loaded!"):format(val) )
    else
      rna:show_status( ("KangarooX120: Bank %.2d is empty. Please, save before a Bank!"):format(val) )
    end
  else
    rna:show_status( "KangarooX120: The \"banks\" folder does not exist! Save a bank first!" )
  end
end

--revise bank
KNG_REVISE_BANK_STATE = true
function kng_revise_bank( bank_1, bank_2 )
  --check folder "banks"
  if ( io.exists("banks") == true ) then
    for i = bank_1, bank_2 do
      --create neutral doc ( it is necessary for invoke after the doc:load_from() ), and not save!
      local doc = renoise.Document.create("Kng_Bank_"..i.."") {
        state = false,
        nme = "",
        nte = { 0 },
        ins = { 0 },
        trk = { 0 },
        vel = { 0 },
        clr = 1
      }
      --load doc to restore
      doc:load_from("banks/bank_"..i..".xml")
      --check state, and change "active" of load button and "name" of textfild
      if ( doc:property("state").value == true ) then
        vws["KNG_BANK_BT_LOAD_"..i].active = true
        vws["KNG_BANK_TXF_"..i].text = doc:property("nme").value
      else
        vws["KNG_BANK_BT_LOAD_"..i].active = false
        vws["KNG_BANK_TXF_"..i].text = ("Bank %.2d"):format( i )
      end
    end
    if ( KNG_REVISE_BANK_STATE == false ) then
      return
    else
      rna:show_status ("KangarooX120: 96 banks have been revised!" )
      KNG_REVISE_BANK_STATE = false
    end
  else
    rna:show_status( "KangarooX120: The \"banks\" folder does not exist! No banks!" )
  end
end

(joule) #5

I won’t study the script in detail, but like i said before:

DON’T create a lot of viewbuilder objects in a scope that Renoise is running when the tool loads. As soon as the file is loaded, these objects will get created which consumes both time and memory for no reason. This will then even bloat the actual startup of Renoise.

It’s very simple to wrap these ViewBuilder object creations inside functions that are called on a need-to-use basis.


(Raul (ulneiz)) #6

It’s very simple to wrap these ViewBuilder object creations inside functions that are called on a need-to-use basis.

I’m trying to get used to this approach.Although the tools are not heavy and the performance is hardly affected, I want to make my tools as good as possible.The problem that I have, is that I use several LUA files and also several global ones, and some of them are ViewBuilder objects (surprisingly most are buttons with different shapes and uses).

Then, I would have to study how to put all these objects inside the functions to wrap them. I guess you mean that. For example, I use this:

XXX = vb:button {
}

--and then

YYY = vb:row {
  XXX,
  vb:button {
  },
  vb:button {
  }
}

And probably, one of those global ones is inside another LUA file.The tool I’m building has a 120-button pad and another 120 buttons that make up a virtual piano, basically, among a few other controls. Both button panels are defined by classes. Actually, I do not notice any performance problems, or initial load. Everything is going very fast.How important is the impact on the performance of loading the Viewbuilder objects?


(joule) #7

I know that some tools can add noticable startup time, but I don’t know if it’s due to bytecode compiling or bad code. I have encountered memory leaks due to non-collected viewbuilder objects though. You can get a tools memory usage by using collectgarbage (google).


(Raul (ulneiz)) #8

I know that some tools can add noticable startup time, but I don’t know if it’s due to bytecode compiling or bad code. I have encountered memory leaks due to non-collected viewbuilder objects though. You can get a tools memory usage by using collectgarbage (google).

I have been manipulating the code of my last tool checking the use of memory with collectgarbabe.I have come to the following conclusion:

I can build the tool in 2 ways (I’ve already checked):

1- The first loads all ViewVuilder functions and objects in memory, since the Renoise boot, and stays there all the time. In this way, it is possible to easily use the IDs of each object to manipulate it. In this way, it is possible to close the tool and reopen it and do not lose “the state” of the object. This approach makes my tool return about 2800KB with collectgarbage. But, it has two advantages. The first is once loaded in memory, always going very fast. The second is that it does not lose the state of each object. Here I use globals and locals to define the objects (without wrapping them in a function).

2- The second way is by wrapping each object definition within a function, which ends with the return of the object.Collectgarbaje returns about 750KB ,a saving of about 2MB.However, the typical error of ID already registered appears, and before loading each object I have to force to equal each identifier = nil.This solves the problem, but it has a drawback. The tool loses the status of the object when it is closed. Since the tool has a multitude of controls, it is not desirable to lose the configuration of each object.I do not know if there is any way to avoid the ID error already registered and that the tool keeps the “state of the objects” after closing it and reopening it again, because I put the id = nil in each object.

On the other hand, I have done collectgarbaje tests with other tools. For example, Duplex returns 18688.289 KBytes (approximately 18.6MB). VoiceRunner1437.452KB (1.43MB).If a tool generates <20MB, it seems reasonable.From 1 to 5MB it seems a pretty low figure.

On the other hand, a tiny delay when loading Renoise is acceptable if after, the tool is loaded very fast (to open it).In the first case, the tool is executed instantaneously, since everything is loaded in the memory. The second case, there is a slight initial delay. Then, I come to the conclusion that it is convenient to use the memory depending on the use of the tool. If each time you start the tool you have to have an identical object state, the second option is appropriate. If you intend to keep the state of the objects for the duration of the entire Renoise open session, the first option is better.

I need to define with many ID’s a multitude of objects, in order to change their state (color, bitmap, text, etc.). Then it is not advisable to cancel the id previously to reload it again. So I do not know how to merge the two cases, so collectgarbaje return the minimum amount possible (I think it will be around 500KB) and at the same time do not lose the state of the objects when closing and reopening the tool in the same session of Renoise.

All this is a little messy for me yet…


(joule) #9

Ah, good to know.

Here is one of the better concepts on how to build GUIs. You can use a class to separate the gui from logic and make it more readable, maintainable and reusable. Just for inspiration :slight_smile: This kind of structure makes everything very easy to access. You could have a parent App class that is passed into the AppGui class (via args), for example.

Click to view contents
class 'AppGui'
function AppGui:__init(args)
  self.title = args and args.title or ""
  self.vb = renoise.ViewBuilder()
  self.views = { }
  self.dialog = nil
end
function AppGui:make_gui()
  local vb = self.vb
  self.views["button"] = vb:button {
    text = "button",
    notifier = function()
      self.views.some_text.text = "Button was pressed"
    end
  }
  self.views["some_text"] = vb:text { }
  self.views["main"] = vb:row {
    self.views["button"],
    self.views["some_text"]
  }
end
function AppGui:show_gui()
  self:make_gui()
  self.dialog = renoise.app():show_custom_dialog(self.title, self.views.main)
end
renoise.tool():add_menu_entry {
  name = "Main Menu:Tools:GUI class",
  invoke = function()
    AppGui { title = "Hello" }:show_gui()
  end
}