From 23a913756846ac88cdb4bcfb4926fcb01029fba9 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Sun, 6 Jul 2025 21:20:41 +0300 Subject: [PATCH 01/18] removal --- .../source/AppPrefs.lua | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 Tools/com.renoise.Sononymph.xrnx/source/AppPrefs.lua diff --git a/Tools/com.renoise.Sononymph.xrnx/source/AppPrefs.lua b/Tools/com.renoise.Sononymph.xrnx/source/AppPrefs.lua deleted file mode 100644 index 1495e730..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/AppPrefs.lua +++ /dev/null @@ -1,19 +0,0 @@ -class 'AppPrefs'(renoise.Document.DocumentNode) - ---------------------------------------------------------------------------------------------------- --- constructor, initialize with default values - -function AppPrefs:__init() - - renoise.Document.DocumentNode.__init(self) - - self:add_property("autostart", renoise.Document.ObservableBoolean(false)) - self:add_property("polling_interval", renoise.Document.ObservableNumber(1)) - self:add_property("path_to_exe", renoise.Document.ObservableString("")) - self:add_property("path_to_config", renoise.Document.ObservableString("")) - self:add_property("show_transfer_warning", renoise.Document.ObservableBoolean(true)) - self:add_property("show_search_warning", renoise.Document.ObservableBoolean(true)) - self:add_property("show_prefs", renoise.Document.ObservableBoolean(true)) - -end - From 469c339579c70b4207e169f77aca457bb4beebca Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Sun, 6 Jul 2025 21:20:56 +0300 Subject: [PATCH 02/18] removal --- .../source/AppUIAbout.lua | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 Tools/com.renoise.Sononymph.xrnx/source/AppUIAbout.lua diff --git a/Tools/com.renoise.Sononymph.xrnx/source/AppUIAbout.lua b/Tools/com.renoise.Sononymph.xrnx/source/AppUIAbout.lua deleted file mode 100644 index 6956746c..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/AppUIAbout.lua +++ /dev/null @@ -1,55 +0,0 @@ -class 'AppUIAbout' (vDialog) - -AppUIAbout.DIALOG_W = 400 -AppUIAbout.TXT_ABOUT = [[This tool adds basic Sononym integration to Renoise. Use it to: -• Search for similar samples (Renoise → Sononym) -• Transfer samples from Sononym → Renoise -• Replace samples in Renoise while browsing in Sononym (auto-transfer) - -NB: LAUNCH SONONYM *BEFORE* USING SEARCH -The tool will require Sononym to be running before launching a search - -otherwise the Sononym process might lock Renoise. If you do this by -accident, simply close the Sononym window and start Sononym -from its usual place (Start Menu, Dock, etc). - -]] - ---------------------------------------------------------------------------------------------------- - -function AppUIAbout:__init(...) - TRACE("AppUIAbout:__init") - - vDialog.__init(self,...) - - local args = cLib.unpack_args(...) - assert(type(args.owner)=="App","Expected 'owner' to be an instance of App") - - local vb = self.vb - - self.dialog_content = vb:column{ - margin = 20, - spacing = 10, - width = AppUIAbout.DIALOG_W, - vb:bitmap{ - bitmap = "source/icons/logo.png", - }, - vb:text{ - text = "ABOUT", - font = "big", - }, - vb:text{ - text = AppUIAbout.TXT_ABOUT - }, - vb:horizontal_aligner{ - mode = "justify", - vb:text{ - text = "Version: "..args.owner.tool_version - }, - vb:button{ - text = "Full Documentation" - } - }, - - } - -end From cb4efe1ecb8d7fde136485296c491666deefb0c0 Mon Sep 17 00:00:00 2001 From: esaruoho Date: Sun, 6 Jul 2025 21:44:15 +0300 Subject: [PATCH 03/18] Sononymph Paketti Modifications --- .gitmodules | 6 - .../com.renoise.Sononymph.xrnx/source/App.lua | 567 ------------------ .../source/AppUI.lua | 471 --------------- Tools/com.renoise.Sononymph.xrnx/source/vLib | 1 - Tools/com.renoise.Sononymph.xrnx/source/xLib | 1 - 5 files changed, 1046 deletions(-) delete mode 100644 Tools/com.renoise.Sononymph.xrnx/source/App.lua delete mode 100644 Tools/com.renoise.Sononymph.xrnx/source/AppUI.lua delete mode 160000 Tools/com.renoise.Sononymph.xrnx/source/vLib delete mode 160000 Tools/com.renoise.Sononymph.xrnx/source/xLib diff --git a/.gitmodules b/.gitmodules index 50b8c464..246aec57 100644 --- a/.gitmodules +++ b/.gitmodules @@ -130,12 +130,6 @@ [submodule "Tools/com.renoise.SelectionShaper.xrnx/source/vLib"] path = Tools/com.renoise.SelectionShaper.xrnx/source/vLib url = https://github.com/renoise/vLib.git -[submodule "Tools/com.renoise.Sononymph.xrnx/source/xLib"] - path = Tools/com.renoise.Sononymph.xrnx/source/xLib - url = https://github.com/renoise/xLib.git -[submodule "Tools/com.renoise.Sononymph.xrnx/source/vLib"] - path = Tools/com.renoise.Sononymph.xrnx/source/vLib - url = https://github.com/renoise/vLib.git [submodule "Tools/com.renoise.Sononymph.xrnx/source/cLib"] path = Tools/com.renoise.Sononymph.xrnx/source/cLib url = https://github.com/renoise/cLib.git diff --git a/Tools/com.renoise.Sononymph.xrnx/source/App.lua b/Tools/com.renoise.Sononymph.xrnx/source/App.lua deleted file mode 100644 index 3431e6c5..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/App.lua +++ /dev/null @@ -1,567 +0,0 @@ - -class "App" - ---------------------------------------------------------------------------------------------------- - -function App:__init(...) - TRACE("App:__init(...)",...) - - local args = cLib.unpack_args(...) - - self.tool_name = args.tool_name - self.tool_version = args.tool_version - - self.monitor_active = property(self.get_monitor_active,self.set_monitor_active) - self.monitor_active_observable = renoise.Document.ObservableBoolean(true) - - -- fire when selection has changed - self.selection_in_sononym_observable = renoise.Document.ObservableBang() - - -- true while live transfer is enabled - self.live_transfer_observable = renoise.Document.ObservableBoolean(false) - - -- table, selectedFile entry from sononym configuration - -- { - -- filename (string) - -- locationPath (string) - -- } - self.selection_in_sononym = {} - - -- - self.paths_are_valid = property(self.get_paths_are_valid) - self.paths_are_valid_observable = renoise.Document.ObservableBoolean(false) - - self.invalid_path_observable = renoise.Document.ObservableString("") - - -- AppPrefs - self.prefs = args.prefs - - -- cFileMonitor, enable when establishing monitoring - self.filemon = cFileMonitor{ - polling_interval = self.prefs.polling_interval.value, - } - - --- configure user-interface - self.ui = AppUI{ - dialog_title = self.app_display_name, - owner = self, - waiting_to_show_dialog = args.waiting_to_show_dialog, - } - - -- notifications ------------------------------ - - self.filemon.changed_observable:add_notifier(function() - TRACE("*** filemon.changed_observable fired...") - - local selection = App.parse_config(self.prefs.path_to_config.value) - if selection then - self.selection_in_sononym = selection - self.selection_in_sononym_observable:bang() - if self.live_transfer_observable.value then - local success,err = self:do_transfer() - if not success then - LOG(err) - return - end - self.ui.update_requested = true - end - end - end) - - self.prefs.polling_interval:add_notifier(function() - self.filemon.polling_interval = self.prefs.polling_interval.value - end) - - self.live_transfer_observable:add_notifier(function() - if self.live_transfer_observable.value then - local success,err = self:do_transfer() - if not success then - LOG(err) - end - end - end) - - self.prefs.path_to_exe:add_notifier(function() - self:check_paths() - end) - self.prefs.path_to_config:add_notifier(function() - self:check_paths() - end) - - -- initialize - self:check_paths() - - local success,err = self:start_monitoring() - if not success and err then - LOG(err) - end - - -end - ---------------------------------------------------------------------------------------------------- --- Properties ---------------------------------------------------------------------------------------------------- - -function App:get_paths_are_valid() - return self.paths_are_valid_observable.value -end - ---------------------------------------------------------------------------------------------------- - -function App:get_monitor_active() - return self.monitor_active_observable.value -end - -function App:set_monitor_active(val) - self.monitor_active_observable.value = val -end - ---------------------------------------------------------------------------------------------------- --- Class methods ---------------------------------------------------------------------------------------------------- --- @return boolean, false when preconditions failed - -function App:toggle_monitoring() - TRACE("App:toggle_monitoring()") - - if not self.monitor_active then - return self:start_monitoring() - else - return self:stop_monitoring() - end - -end - ---------------------------------------------------------------------------------------------------- --- start monitoring --- @return boolean, false when preconditions failed - -function App:start_monitoring() - TRACE("App:start_monitoring()") - - if not self.paths_are_valid then - return false - end - - self.filemon.paths = { - --self.prefs.path_to_exe.value, - self.prefs.path_to_config.value, - } - self.filemon:start() - self.monitor_active = true - return true - -end - ---------------------------------------------------------------------------------------------------- --- stop monitoring --- @return boolean, false when preconditions failed - -function App:stop_monitoring() - self.filemon:stop() - self.monitor_active = false - return true -end - ---------------------------------------------------------------------------------------------------- --- enable/disable live transfer (Sononym to Renoise) --- @return boolean, false when preconditions failed - -function App:toggle_live_transfer() - TRACE("App:toggle_live_transfer()") - - if not self.paths_are_valid then - return false - end - - if not self.live_transfer_observable.value - and self.prefs.show_transfer_warning.value - then - local choice = renoise.app():show_prompt("Enable auto-transfer?","" - .."Auto-transfer will automatically replace the selected sample - " - .."\nare you sure you want to enable this feature?", - {"Yes","Yes, and don't show this warning","Cancel"}) - if (choice == "Cancel") then - return false - elseif (choice == "Always (don't show warning)") then - self.prefs.show_transfer_warning.value = false - end - end - - self.live_transfer_observable.value = not self.live_transfer_observable.value - - return true - -end - ---------------------------------------------------------------------------------------------------- --- check paths and update "paths_are_valid" with result - -function App:check_paths() - TRACE("App:check_paths()") - - local path = self.prefs.path_to_exe.value - local success,err = App.check_path(path) - if not success then - self.invalid_path_observable.value = path - self.paths_are_valid_observable.value = false - return - end - - local path = self.prefs.path_to_config.value - local success,err = App.check_path(path) - if not success then - self.invalid_path_observable.value = path - self.paths_are_valid_observable.value = false - return - end - - self.invalid_path_observable.value = "" - self.paths_are_valid_observable.value = true - -end - - ---------------------------------------------------------------------------------------------------- --- auto-configure tool (invoked when showing GUI without proper configuration) --- @return boolean, true when - -function App:autoconfigure() - TRACE("App:autoconfigure()") - - local success,err = self:set_path_to_exe(App.guess_path_to_exe()) - if not success then - return false,err - end - - local success,err = self:set_path_to_config(App.guess_path_to_config()) - if not success then - return false,err - end - - return true - -end - ---------------------------------------------------------------------------------------------------- - -function App:pick_path_to_exe() - TRACE("App:pick_path_to_exe()") - - local platform = os.platform() - local suggested_path = nil - local ext = {"*.*"} - if (platform == "WINDOWS") then - ext = {"Sononym.exe"} - elseif (platform == "MACINTOSH") then - ext = {"Sononym"} - elseif (platform == "LINUX") then - end - - local title = "Choose location of Sononym executable" - local file_path = renoise.app():prompt_for_filename_to_read(ext,title) - if (file_path == "") then - return - end - - self:set_path_to_exe(file_path) - -end - ---------------------------------------------------------------------------------------------------- - -function App:set_path_to_exe(file_path) - TRACE("App:set_path_to_exe(file_path)",file_path) - - file_path = cFilesystem.unixslashes(file_path) - self.prefs.path_to_exe.value = file_path - local success,err = App.check_path(file_path) - if not success then - self:stop_monitoring() - return false,err - end - return true -end - ---------------------------------------------------------------------------------------------------- - -function App:pick_path_to_config() - TRACE("App:pick_path_to_config()") - - local ext = {"query.json"} - local title = "Choose location of Sononym configuration" - local file_path = renoise.app():prompt_for_filename_to_read(ext,title) - if (file_path == "") then - return - end - - self:set_path_to_config(file_path) - -end - ---------------------------------------------------------------------------------------------------- - -function App:set_path_to_config(file_path) - TRACE("App:set_path_to_config(file_path)",file_path) - - file_path = cFilesystem.unixslashes(file_path) - self.prefs.path_to_config.value = file_path - local success,err = App.check_path(file_path) - if not success then - self:stop_monitoring() - return false,err - end - -- immediately start monitoring - local success,err = self:start_monitoring() - if not success and err then - LOG(err) - return false,err - end - return true - -end - - ---------------------------------------------------------------------------------------------------- --- apply the currently selected file in Sononym to the selection in Renoise --- TODO replace *selected range* in sample - -function App:do_transfer() - TRACE("App:do_transfer()") - - -- if any of these are true, instrument gets name of sample - local created_instrument = false - local instr_named_after_sample = false - - local sample,instr = rns.selected_sample,rns.selected_instrument - if not sample then - sample = instr:insert_sample_at(1) - created_instrument = true - else - if (sample.name == instr.name) then - instr_named_after_sample = true - end - end - - if table.is_empty(self.selection_in_sononym) then - return false,"Please define a valid path to the Sononym configuration" - .. "\n(see tool preferences)" - end - - -- combine filename + locationPath - local config_folder,_,__ = cFilesystem.get_path_parts(self.prefs.path_to_config.value) - local folder,_,__ = cFilesystem.get_path_parts(self.selection_in_sononym.locationPath) - if (folder == config_folder) then - -- internal sononym library means filename is absolute - folder = "" - end - local fpath = string.format("%s%s",folder,self.selection_in_sononym.filename) - - local success,err = pcall(function() - sample.sample_buffer:load_from(fpath) - end) - if not success then - return false,"Failed to load sample:\n"..tostring(err) - end - - -- update samplename - local folder,filename,ext = cFilesystem.get_path_parts(self.selection_in_sononym.filename) - sample.name = filename - - if created_instrument or instr_named_after_sample then - instr.name = filename - end - - -- display message in status bar / terminal - local msg = "Transferred sample: "..fpath - renoise.app():show_status(msg) - LOG(msg) - - return true - -end - ---------------------------------------------------------------------------------------------------- --- save the selected sample and launch a similarity search --- TODO option to save only the *selected range* --- @return boolean, true or false,string when failed - -function App:do_search() - TRACE("App:do_search()") - - -- show important notice the first time - if self.prefs.show_search_warning.value then - local choice = renoise.app():show_prompt("Important notice","" - .."Please make sure that Sononym is running before launching a search" - .."\n(NB: this message is only shown once!)" - ,{"Start search","Cancel"}) - if (choice == "Cancel") then - return false - else - self.prefs.show_search_warning.value = false - end - end - - local success,err = App.check_path(self.prefs.path_to_exe.value) - if not success then - return false,"Please define a valid path to the Sononym executable" - .."\n(see tool preferences)" - end - - local tmp_path,err = self:_create_temp() - if not tmp_path then - return false,"Unable to launch search: " .. err - end - - local path_to_exe = cFilesystem.unixslashes(self.prefs.path_to_exe.value) - local cmd = string.format('"%s" %s',path_to_exe,cFilesystem.unixslashes(tmp_path)) - local code = os.execute(cmd) - - return true - -end - ---------------------------------------------------------------------------------------------------- --- save a copy of the selected sample to the temporary folder --- @return string, temp filename or nil,string if failed - -function App:_create_temp() - - if not rns.selected_sample then - return nil,"No sample is selected" - end - - local buffer = xSample.get_sample_buffer(rns.selected_sample) - if not buffer then - return nil,"Sample is empty" - end - local tmp_path = os.tmpname("flac") - local success = buffer:save_as(tmp_path,"flac") - if not success then - return nil,"Failed to save sample" - end - return tmp_path -end - - ---------------------------------------------------------------------------------------------------- - -function App:detach_sampler() - TRACE("App:detach_sampler()") - - local enum_sampler = renoise.ApplicationWindow.MIDDLE_FRAME_INSTRUMENT_SAMPLE_EDITOR - local middle_frame = renoise.app().window.active_middle_frame - renoise.app().window.instrument_editor_is_detached = true - renoise.app().window.active_middle_frame = enum_sampler - renoise.app().window.active_middle_frame = middle_frame - -end - ---------------------------------------------------------------------------------------------------- --- Static methods ---------------------------------------------------------------------------------------------------- --- Parse sononym configuration file (query.json) to find currently selected path --- @param path (string) --- @return table { --- filename (string) --- locationPath (string) ---} --- or nil,error message (string) - -function App.parse_config(path) - TRACE("App.parse_config(path)") - - -- return error if no path is supplied - if not path or (path == "") then - return nil,"No path is supplied" - end - - -- load the config file - local fhandle = io.open(path,"r") - if not fhandle then - fhandle:close() - return nil, "ERROR: Failed to open file handle" - end - - local str_json = fhandle:read("*a") - fhandle:close() - - -- parse the string - local first,last = nil,nil - local offset = string.find(str_json,'"selectedFile"') - str_json = string.sub(str_json,offset) - - first,last = string.find(str_json,'"filename":%C*') - local filename = cFilesystem.unixslashes(string.sub(str_json,first+13,last-2)) - - first,last = string.find(str_json,'"locationPath":%C*') - local locationPath = cFilesystem.unixslashes(string.sub(str_json,first+17,last-1)) - - return { - filename = filename, - locationPath = locationPath, - } - -end - ---------------------------------------------------------------------------------------------------- --- check if path is valid and existing --- @return boolean - -function App.check_path(str_path) - TRACE("App.check_path(str_path)",str_path) - - if not str_path or (str_path == "") then - return false,"No path specified" - end - - if not io.exists(str_path) then - return false,"Path does not exist" - end - - return true - -end - ---------------------------------------------------------------------------------------------------- --- attempt to resolve the location of the sononym executable - -function App.guess_path_to_exe() - TRACE("App.guess_path_to_exe()") - - local platform = os.platform() - if (platform == "WINDOWS") then - -- spawn terminal to obtain windows environment variable - local cmd = "echo %PROGRAMFILES%" - local f = assert(io.popen(cmd, 'r')) - local s = assert(f:read('*a')) - f:close() - return cFilesystem.unixslashes(cString.trim(s).."/Sononym/Sononym.exe") - elseif (platform == "MACINTOSH") then - return "/Applications/Sononym.app/Contents/MacOS/Sononym" - elseif (platform == "LINUX") then - error("not implemented") - end - -end - ---------------------------------------------------------------------------------------------------- --- attempt to resolve the location of 'query.json' - -function App.guess_path_to_config() - TRACE("App.guess_path_to_config()") - - local platform = os.platform() - if (platform == "WINDOWS") then - return cFilesystem.get_user_folder() .. "AppData/Roaming/Sononym/query.json" - elseif (platform == "MACINTOSH") then - return cFilesystem.get_user_folder() .. "Library/Application Support/Sononym/query.json" - elseif (platform == "LINUX") then - error("not implemented") - end - -end - - - diff --git a/Tools/com.renoise.Sononymph.xrnx/source/AppUI.lua b/Tools/com.renoise.Sononymph.xrnx/source/AppUI.lua deleted file mode 100644 index ecd69543..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/AppUI.lua +++ /dev/null @@ -1,471 +0,0 @@ - -class "AppUI" (vDialog) - -AppUI.LABEL_W = 85 -AppUI.INPUT_W = 250 -AppUI.BUTTON_W = 130 -AppUI.DIALOG_W = AppUI.LABEL_W + AppUI.INPUT_W + AppUI.BUTTON_W -AppUI.RENOISE_PLACEHOLDER = "No sample selected" -AppUI.SONONYM_PLACEHOLDER = "-" - - ---------------------------------------------------------------------------------------------------- - -function AppUI:__init(...) - TRACE("AppUI:__init") - - local args = cLib.unpack_args(...) - assert(type(args.owner)=="App","Expected 'owner' to be an instance of App") - - vDialog.__init(self,...) - - -- App - self.owner = args.owner - - -- vDialog - self.about_dialog = nil - - -- vToggleButton - self.prefs_toggle = nil - - -- notifications ------------------------------ - - self.owner.monitor_active_observable:add_notifier(function() - self.update_requested = true - end) - - self.owner.prefs.path_to_exe:add_notifier(function() - self.update_requested = true - end) - - self.owner.prefs.path_to_config:add_notifier(function() - self.update_requested = true - end) - - self.owner.prefs.show_prefs:add_notifier(function() - self.update_requested = true - end) - - self.owner.selection_in_sononym_observable:add_notifier(function() - self.update_requested = true - end) - - self.owner.live_transfer_observable:add_notifier(function() - self.update_requested = true - end) - - self.owner.paths_are_valid_observable:add_notifier(function() - self.update_requested = true - end) - - - -- initialize - - renoise.tool().app_idle_observable:add_notifier(self,self.on_idle) - renoise.tool().app_new_document_observable:add_notifier(function() - self:attach_to_song() - end) - - self:attach_to_song() - -end - ---------------------------------------------------------------------------------------------------- --- vDialog methods (overridden) - -function AppUI:create_dialog() - TRACE("AppUI:create_dialog()") - - local vb = self.vb - - self.prefs_toggle = vToggleButton{ - vb = vb, - enabled = self.owner.prefs.show_prefs.value, - text_enabled = "▾", - text_disabled = "▴", - notifier = function() - self.owner.prefs.show_prefs.value = not self.owner.prefs.show_prefs.value - end - } - - return vb:column{ - margin = 4, - spacing = 4, - vb:column{ - margin = 6, - spacing = 4, - style = "group", - vb:row{ - vb:text{ - text = "Renoise", - width = AppUI.LABEL_W - 18, - }, - vb:button{ - bitmap = "./source/icons/detach.bmp", - notifier = function() - self.owner:detach_sampler() - end - }, - vb:row{ - style = "plain", - vb:text{ - id = "sample_name_renoise", - --font = "mono", - text = AppUI.RENOISE_PLACEHOLDER, - width = AppUI.INPUT_W, - - }, - }, - vb:button{ - id = "bt_search", - text = "Search in Sononym", - tooltip = "Click to launch a similarity search on this sample", - --height = 30, - width = AppUI.BUTTON_W, - notifier = function() - local success,err = self.owner:do_search() - if not success then - renoise.app():show_message(err) - end - end - }, - }, - vb:row{ - vb:column{ - vb:text{ - id = "label_filename_sononym", - text = "Sononym", - width = AppUI.LABEL_W, - }, - vb:row{ - self.prefs_toggle.view, - vb:text{ - text = "Options", - font = "bold", - }, - }, - - }, - vb:column{ - vb:row{ - style = "plain", - vb:text{ - id = "filename_sononym", - --font = "mono", - text = AppUI.SONONYM_PLACEHOLDER, - width = AppUI.INPUT_W, - }, - }, - vb:row{ - --style = "plain", - vb:text{ - id = "location_path_sononym", - --font = "mono", - text = "In folder:", - }, - }, - }, - vb:column{ - vb:button{ - id = "bt_transfer", - text = "Transfer to Renoise", - tooltip = "Click to transfer the selected sample from Sononym", - --height = 30, - width = AppUI.BUTTON_W, - notifier = function() - local success,err = self.owner:do_transfer() - if not success then - renoise.app():show_message(err) - end - end - }, - vb:row{ - vb:checkbox{ - id = "cb_transfer_toggle", - --text = "", - notifier = function() - local success,err = self.owner:toggle_live_transfer() - if not success and err then - renoise.app():show_message(err) - end - end - }, - vb:text{ - text = "Auto-transfer" - }, - }, - - }, - - }, - - }, - vb:column{ - style = "group", - id = "preferences_content", - margin = 6, - vb:horizontal_aligner{ - mode = "justify", - width = AppUI.DIALOG_W, - vb:row{ - vb:text{ - text = "Autostart tool", - width = AppUI.LABEL_W, - }, - vb:checkbox{ - bind = self.owner.prefs.autostart - }, - }, - vb:button{ - text = "How to use", - width = AppUI.BUTTON_W, - notifier = function() - self:launch_howto() - end - } - }, - vb:space{ - width = AppUI.DIALOG_W, - }, - vb:column{ - vb:row{ - vb:text{ - text = "Path to exe", - width = AppUI.LABEL_W, - }, - vb:row{ - style = "plain", - vb:textfield{ - id = "path_to_exe", - text = "", - width = AppUI.INPUT_W, - notifier = function(txt) - local success,err = self.owner:set_path_to_exe(txt) - if not success then - renoise.app():show_warning(err) - end - end - }, - }, - vb:button{ - text = "Detect", - width = AppUI.BUTTON_W/2, - notifier = function() - local choice = renoise.app():show_prompt("Auto-detect path", - "This will attempt to auto-detect the path to the Sononym executable. " - .."\nAre you sure you want to do this?", - {"OK","Cancel"}) - if (choice == "OK") then - local success,err = self.owner:set_path_to_exe(App.guess_path_to_exe()) - if not success then - if err then - renoise.app():show_warning(err) - end - end - end - end - }, - vb:button{ - text = "Browse...", - width = AppUI.BUTTON_W/2, - notifier = function() - self.owner:pick_path_to_exe() - end - }, - }, - vb:row{ - vb:text{ - text = "Path to config", - width = AppUI.LABEL_W, - }, - vb:row{ - style = "plain", - vb:textfield{ - id = "path_to_config", - text = "", - width = AppUI.INPUT_W, - notifier = function(txt) - local success,err = self.owner:set_path_to_config(txt) - if not success then - renoise.app():show_warning(err) - end - end - }, - }, - vb:button{ - text = "Detect", - width = AppUI.BUTTON_W/2, - notifier = function() - local choice = renoise.app():show_prompt("Auto-detect path", - "This will attempt to auto-detect the path to the Sononym configuration. " - .."\nAre you sure you want to do this?", - {"OK","Cancel"}) - if (choice == "OK") then - local txt = vb.views["path_to_config"].text - local success,err = self.owner:set_path_to_config(App.guess_path_to_config()) - if not success then - if err then - renoise.app():show_warning(err) - end - end - end - end - }, - vb:button{ - text = "Browse...", - width = AppUI.BUTTON_W/2, - notifier = function() - self.owner:pick_path_to_config() - end - }, - }, - vb:row{ - vb:text{ - text = "Status ", - width = AppUI.LABEL_W, - }, - vb:text{ - text = "", - id = "txt_tool_status", - }, - }, - - }, - - }, - - } - -end - ---------------------------------------------------------------------------------------------------- - -function AppUI:show() - TRACE("AppUI:show") - - if not self.owner.paths_are_valid then - local choice = renoise.app():show_prompt("Configure tool", - "The tool needs to be configured. Do you want to automatically detect" - .."\nappropriate paths for the Sononym executable and configuration?", - {"Yes please!","No, I will enter them manually"}) - if (choice == "Yes please!") then - self.owner:autoconfigure() - end - end - - - vDialog.show(self) - self.update_requested = true - -end - ---------------------------------------------------------------------------------------------------- --- Class methods ---------------------------------------------------------------------------------------------------- - -function AppUI:update() - TRACE("AppUI:update") - - if not self.dialog or not self.dialog.visible then - return - end - - local ctrl - local vb = self.vb - - local buffer = rns.selected_sample - and xSample.get_sample_buffer(rns.selected_sample) - local samplename = rns.selected_sample - and xSample.get_display_name(rns.selected_sample,rns.selected_sample_index) - if samplename and not buffer then - samplename = samplename .. " (empty)" - end - ctrl = vb.views["sample_name_renoise"] - ctrl.text = samplename or AppUI.RENOISE_PLACEHOLDER - ctrl.tooltip = ctrl.text - ctrl.width = AppUI.INPUT_W - - local filename = self.owner.selection_in_sononym and self.owner.selection_in_sononym.filename - ctrl = vb.views["filename_sononym"] - ctrl.text = filename or AppUI.SONONYM_PLACEHOLDER - ctrl.tooltip = ctrl.text - ctrl.width = AppUI.INPUT_W - - local location_path = self.owner.selection_in_sononym.locationPath - and cFilesystem.get_path_parts(self.owner.selection_in_sononym.locationPath) - ctrl = vb.views["location_path_sononym"] - ctrl.text = "Library: ".. (location_path or AppUI.SONONYM_PLACEHOLDER) - ctrl.tooltip = ctrl.text - ctrl.width = AppUI.INPUT_W - - local path_to_exe = self.owner.prefs.path_to_exe.value - ctrl = vb.views["path_to_exe"] - ctrl.text = path_to_exe - ctrl.tooltip = ctrl.text - ctrl.width = AppUI.INPUT_W - - local path_to_config = self.owner.prefs.path_to_config.value - ctrl = vb.views["path_to_config"] - ctrl.text = path_to_config - ctrl.tooltip = ctrl.text - ctrl.width = AppUI.INPUT_W - - ctrl = vb.views["bt_transfer"] - ctrl.active = not self.owner.live_transfer_observable.value - - ctrl = vb.views["preferences_content"] - ctrl.visible = self.owner.prefs.show_prefs.value - - ctrl = self.vb.views["txt_tool_status"] - local paths_are_valid = self.owner.paths_are_valid_observable.value - local monitor_active = self.owner.monitor_active_observable.value - ctrl.text = (paths_are_valid and monitor_active) - and "✔ Monitoring for changes..." - or "⚠ Invalid path: "..self.owner.invalid_path_observable.value - -end - ---------------------------------------------------------------------------------------------------- ---- handle idle notifications - -function AppUI:on_idle() - - if self.update_requested then - self.update_requested = false - self:update() - end - - local is_visible = self:dialog_is_visible() - if not is_visible and self.owner.monitor_active then - self.owner:stop_monitoring() - elseif is_visible and not self.owner.monitor_active then - self.owner:start_monitoring() - end - -end - ---------------------------------------------------------------------------------------------------- - -function AppUI:launch_howto() - - if not self.about_dialog then - self.about_dialog = AppUIAbout{ - owner = self.owner - } - end - - self.about_dialog:show() - -end - ---------------------------------------------------------------------------------------------------- --- invoke when a new document becomes available - -function AppUI:attach_to_song() - - rns.selected_sample_observable:add_notifier(function() - self.update_requested = true - end) - -end - diff --git a/Tools/com.renoise.Sononymph.xrnx/source/vLib b/Tools/com.renoise.Sononymph.xrnx/source/vLib deleted file mode 160000 index 83a5ee97..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/vLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 83a5ee97cf1694623561b6eb5f5dd873078ea4d9 diff --git a/Tools/com.renoise.Sononymph.xrnx/source/xLib b/Tools/com.renoise.Sononymph.xrnx/source/xLib deleted file mode 160000 index 36fb0f47..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/xLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 36fb0f47518d40b516d71c78f975455f15f4dc82 From 4e3fc388d84eccfe57bd42e4041856376ddf2afe Mon Sep 17 00:00:00 2001 From: esaruoho Date: Sun, 6 Jul 2025 21:46:38 +0300 Subject: [PATCH 04/18] Sononymph: Remove cLib submodule, vendor directly --- Tools/com.renoise.Sononymph.xrnx/source/cLib | 1 - 1 file changed, 1 deletion(-) delete mode 160000 Tools/com.renoise.Sononymph.xrnx/source/cLib diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib b/Tools/com.renoise.Sononymph.xrnx/source/cLib deleted file mode 160000 index e5b1f052..00000000 --- a/Tools/com.renoise.Sononymph.xrnx/source/cLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e5b1f052b20885dd40ae12dbde3d193a98249b0d From a6cc8e2fdadee9802e11499ff10e230cfc19ccf4 Mon Sep 17 00:00:00 2001 From: esaruoho Date: Sun, 6 Jul 2025 21:50:52 +0300 Subject: [PATCH 05/18] Sononymph Paketti Modifications - complete version 0.91 --- Tools/com.renoise.Sononymph.xrnx/App.lua | 1497 +++++++++++++++++ Tools/com.renoise.Sononymph.xrnx/AppUI.lua | 605 +++++++ Tools/com.renoise.Sononymph.xrnx/README.md | 134 +- Tools/com.renoise.Sononymph.xrnx/changelog.md | 64 + Tools/com.renoise.Sononymph.xrnx/main.lua | 223 ++- Tools/com.renoise.Sononymph.xrnx/manifest.xml | 16 +- 6 files changed, 2374 insertions(+), 165 deletions(-) create mode 100644 Tools/com.renoise.Sononymph.xrnx/App.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/AppUI.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/changelog.md diff --git a/Tools/com.renoise.Sononymph.xrnx/App.lua b/Tools/com.renoise.Sononymph.xrnx/App.lua new file mode 100644 index 00000000..0f24e0a0 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/App.lua @@ -0,0 +1,1497 @@ +class "App" +--------------------------------------------------------------------------------------------------- +function App:__init(...) + TRACE("App:__init(...)",...) + + local args = cLib.unpack_args(...) + + self.tool_name = args.tool_name + self.tool_version = args.tool_version + self.app_display_name = "Sononymph - Paketti Modifications v" .. self.tool_version + + self.monitor_active = property(self.get_monitor_active,self.set_monitor_active) + self.monitor_active_observable = renoise.Document.ObservableBoolean(true) + + -- fire when selection has changed + self.selection_in_sononym_observable = renoise.Document.ObservableBang() + + -- true while live transfer is enabled + self.live_transfer_observable = renoise.Document.ObservableBoolean(false) + + -- table, selectedFile entry from sononym configuration + -- { + -- filename (string) + -- locationPath (string) + -- } + self.selection_in_sononym = {} + + -- + self.paths_are_valid = property(self.get_paths_are_valid) + self.paths_are_valid_observable = renoise.Document.ObservableBoolean(false) + + self.invalid_path_observable = renoise.Document.ObservableString("") + + -- AppPrefs + self.prefs = args.prefs + + -- cFileMonitor, enable when establishing monitoring + self.filemon = cFileMonitor{ + polling_interval = self.prefs.polling_interval.value, + } + + --- configure user-interface + self.ui = AppUI{ + dialog_title = self.app_display_name, + owner = self, + waiting_to_show_dialog = args.waiting_to_show_dialog, + } + + -- notifications ------------------------------ + + self.filemon.changed_observable:add_notifier(function() + local selection = App.parse_config(self.prefs.path_to_config.value) + if selection then + self.selection_in_sononym = selection + self.selection_in_sononym_observable:bang() + if self.live_transfer_observable.value then + local success,err = self:do_transfer() + if not success then + LOG(err) + return + end + self.ui.update_requested = true + renoise.app().window.active_middle_frame=5 + end + end + end) + + self.prefs.polling_interval:add_notifier(function() + self.filemon.polling_interval = self.prefs.polling_interval.value + end) + + self.live_transfer_observable:add_notifier(function() + if self.live_transfer_observable.value then + local success,err = self:do_transfer() + if not success then + LOG(err) + end + end + end) + + self.prefs.path_to_exe:add_notifier(function() + self:check_paths() + end) + self.prefs.path_to_config:add_notifier(function() + self:check_paths() + end) + + -- add notifier to refresh menu when monitor state changes + self.monitor_active_observable:add_notifier(function() + register_tool_menu() -- refresh the menu when monitoring state changes + end) + + -- initialize + self:check_paths() + + local success,err = self:start_monitoring() + if not success and err then + LOG(err) + end + + +end + +--------------------------------------------------------------------------------------------------- +-- Properties +--------------------------------------------------------------------------------------------------- + +function App:get_paths_are_valid() + return self.paths_are_valid_observable.value +end + +--------------------------------------------------------------------------------------------------- + +function App:get_monitor_active() + return self.monitor_active_observable.value +end + +function App:set_monitor_active(val) + self.monitor_active_observable.value = val +end + +--------------------------------------------------------------------------------------------------- +-- Class methods +--------------------------------------------------------------------------------------------------- +-- @return boolean, false when preconditions failed + +function App:toggle_monitoring() + TRACE("App:toggle_monitoring()") + + if not self.monitor_active then + return self:start_monitoring() + else + return self:stop_monitoring() + end + +end + +--------------------------------------------------------------------------------------------------- +-- start monitoring +-- @return boolean, false when preconditions failed + +function App:start_monitoring() + TRACE("App:start_monitoring()") + + if not self.paths_are_valid then + TRACE("App:start_monitoring() - paths are not valid, cannot start monitoring") + return false + end + + self.filemon.paths = { + --self.prefs.path_to_exe.value, + self.prefs.path_to_config.value, + } + -- Start monitoring the query.json file + + self.filemon:start() + self.monitor_active = true + return true + +end + +--------------------------------------------------------------------------------------------------- +-- stop monitoring +-- @return boolean, false when preconditions failed + +function App:stop_monitoring() + self.filemon:stop() + self.monitor_active = false + return true +end + +--------------------------------------------------------------------------------------------------- +-- enable/disable live transfer (Sononym to Renoise) +-- @return boolean, false when preconditions failed + +function App:toggle_live_transfer() + TRACE("App:toggle_live_transfer()") + + if not self.paths_are_valid then + return false + end + + if not self.live_transfer_observable.value + and self.prefs.show_transfer_warning.value + then +--[[ local choice = renoise.app():show_prompt("Enable auto-transfer?","" + .."Auto-transfer will automatically replace the selected sample - " + .."\nare you sure you want to enable this feature?", + {"Yes","Yes, and don't show this warning","Cancel"}) + if (choice == "Cancel") then + return false + elseif (choice == "Always (don't show warning)") then ]]-- + self.prefs.show_transfer_warning.value = false + + end + + self.live_transfer_observable.value = not self.live_transfer_observable.value + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- check paths and update "paths_are_valid" with result + +function App:check_paths() + TRACE("App:check_paths()") + + local path = self.prefs.path_to_exe.value + local success,err = App.check_path(path) + if not success then + self.invalid_path_observable.value = path + self.paths_are_valid_observable.value = false + return + end + + local path = self.prefs.path_to_config.value + local success,err = App.check_path(path) + if not success then + self.invalid_path_observable.value = path + self.paths_are_valid_observable.value = false + return + end + + self.invalid_path_observable.value = "" + self.paths_are_valid_observable.value = true + +end + + +--------------------------------------------------------------------------------------------------- +-- auto-configure tool (invoked when showing GUI without proper configuration) +-- @return boolean, true when + +function App:autoconfigure() + TRACE("App:autoconfigure()") + + local exe_path = App.guess_path_to_exe() + LOG("*** autoconfigure: detected exe path:", exe_path or "nil") + local success,err = self:set_path_to_exe(exe_path) + if not success then + LOG("*** autoconfigure: failed to set exe path:", err) + + -- Provide helpful error message for Linux + local platform = os.platform() + if (platform == "LINUX") then + local helpful_err = "Sononym executable not found. Please:\n" .. + "1. Install Sononym from https://www.sononym.net/\n" .. + "2. Make sure 'sononym' is in your PATH\n" .. + "3. Or manually set the AppPath to the Sononym executable location\n" .. + "Original error: " .. (err or "Path does not exist") + return false, helpful_err + end + + return false,err + end + + local config_path = App.guess_path_to_config() + LOG("*** autoconfigure: detected config path:", config_path or "nil") + if not config_path then + LOG("*** autoconfigure: config path is nil - detection failed") + + -- Provide helpful error message for Linux + local platform = os.platform() + if (platform == "LINUX") then + local helpful_err = "Sononym configuration directory not found. Please:\n" .. + "1. Run Sononym at least once to create the configuration\n" .. + "2. The config should be created at: ~/.config/Sononym/[version]/query.json\n" .. + "3. Or manually set the ConfigPath to your query.json file location" + return false, helpful_err + else + return false, "No Sononym configuration found - please run Sononym first" + end + end + + local success,err = self:set_path_to_config(config_path) + if not success then + LOG("*** autoconfigure: failed to set config path:", err) + return false,err + end + + LOG("*** autoconfigure: success - config path set to:", config_path) + return true + +end + +--------------------------------------------------------------------------------------------------- + +function App:pick_path_to_exe() + TRACE("App:pick_path_to_exe()") + + local platform = os.platform() + local suggested_path = nil + local ext = {"*.*"} + if (platform == "WINDOWS") then + ext = {"Sononym.exe"} + elseif (platform == "MACINTOSH") then + ext = {"Sononym"} + elseif (platform == "LINUX") then + ext = {"sononym"} + end + + local title = "Choose the location of the Sononym app" + local file_path = renoise.app():prompt_for_filename_to_read(ext,title) + if (file_path == "") then + return + end + + self:set_path_to_exe(file_path) + +end + +--------------------------------------------------------------------------------------------------- + +function App:set_path_to_exe(file_path) + TRACE("App:set_path_to_exe(file_path)",file_path) + + file_path = cFilesystem.unixslashes(file_path) + self.prefs.path_to_exe.value = file_path + local success,err = App.check_path(file_path) + if not success then + self:stop_monitoring() + return false,err + end + return true +end + +--------------------------------------------------------------------------------------------------- + +function App:pick_path_to_config() + TRACE("App:pick_path_to_config()") + + local ext = {"query.json"} + local title = "Choose the location of the Sononym configuration file" + local file_path = renoise.app():prompt_for_filename_to_read(ext,title) + if (file_path == "") then + return + end + + self:set_path_to_config(file_path) + +end + +--------------------------------------------------------------------------------------------------- + +function App:set_path_to_config(file_path) + TRACE("App:set_path_to_config(file_path)",file_path) + + file_path = cFilesystem.unixslashes(file_path) + self.prefs.path_to_config.value = file_path + local success,err = App.check_path(file_path) + if not success then + self:stop_monitoring() + return false,err + end + -- immediately start monitoring + local success,err = self:start_monitoring() + if not success and err then + LOG(err) + return false,err + end + return true + +end + + +--------------------------------------------------------------------------------------------------- +-- Helper function to copy sample settings (for slice-aware transfer) +function App:copy_sample_settings(source_sample, target_sample) + if not source_sample or not target_sample then + return false + end + + -- Copy basic sample properties + target_sample.transpose = source_sample.transpose + target_sample.fine_tune = source_sample.fine_tune + target_sample.volume = source_sample.volume + target_sample.panning = source_sample.panning + target_sample.beat_sync_enabled = source_sample.beat_sync_enabled + target_sample.beat_sync_lines = source_sample.beat_sync_lines + target_sample.beat_sync_mode = source_sample.beat_sync_mode + target_sample.new_note_action = source_sample.new_note_action + target_sample.oneshot = source_sample.oneshot + target_sample.mute_group = source_sample.mute_group + target_sample.autoseek = source_sample.autoseek + target_sample.autofade = source_sample.autofade + + -- Copy loop settings + target_sample.loop_mode = source_sample.loop_mode + target_sample.loop_start = math.min(source_sample.loop_start, target_sample.sample_buffer.number_of_frames - 1) + target_sample.loop_end = math.min(source_sample.loop_end, target_sample.sample_buffer.number_of_frames) + + return true +end + +--------------------------------------------------------------------------------------------------- +-- Helper function to copy slice settings (for slice-aware transfer) +function App:copy_slice_settings(source_slice, target_slice) + if not source_slice or not target_slice then + return false + end + + target_slice.transpose = source_slice.transpose + target_slice.fine_tune = source_slice.fine_tune + target_slice.volume = source_slice.volume + target_slice.panning = source_slice.panning + target_slice.beat_sync_enabled = source_slice.beat_sync_enabled + target_slice.beat_sync_lines = source_slice.beat_sync_lines + target_slice.beat_sync_mode = source_slice.beat_sync_mode + target_slice.new_note_action = source_slice.new_note_action + target_slice.oneshot = source_slice.oneshot + target_slice.mute_group = source_slice.mute_group + target_slice.autoseek = source_slice.autoseek + target_slice.autofade = source_slice.autofade + + return true +end + +--------------------------------------------------------------------------------------------------- +-- Slice-aware sample loading (preserves slice markers and settings) +function App:load_sample_with_slice_preservation(sample, fpath) + TRACE("App:load_sample_with_slice_preservation()") + + -- Check if the current sample has slice markers + if #sample.slice_markers == 0 then + -- No slices, use normal loading + TRACE("No slice markers found, using normal loading") + return pcall(function() + sample.sample_buffer:load_from(fpath) + end) + end + + TRACE("Found slice markers, using slice-aware loading") + local original_instrument = rns.selected_instrument + + -- Save slice markers and sample settings + local saved_markers = {} + for _, marker in ipairs(sample.slice_markers) do + table.insert(saved_markers, marker) + end + local saved_sample = sample + + -- Load the new sample + local success, err = pcall(function() + sample.sample_buffer:load_from(fpath) + end) + + if not success then + return false, err + end + + -- Get the new sample length + local new_sample = rns.selected_sample + if not new_sample then + return false, "Failed to get new sample after loading" + end + + local new_sample_length = new_sample.sample_buffer.number_of_frames + + -- Filter markers to fit within the new sample length + local valid_markers = {} + for _, marker in ipairs(saved_markers) do + if marker <= new_sample_length then + table.insert(valid_markers, marker) + end + end + + -- Apply the valid slice markers + if #valid_markers > 0 then + new_sample.slice_markers = valid_markers + + -- Copy sample settings + self:copy_sample_settings(saved_sample, new_sample) + + -- Copy slice settings for each individual slice sample + for i = 2, math.min(#original_instrument.samples, #rns.selected_instrument.samples) do + if original_instrument.samples[i] and rns.selected_instrument.samples[i] then + self:copy_slice_settings(original_instrument.samples[i], rns.selected_instrument.samples[i]) + end + end + + TRACE("Applied", #valid_markers, "slice markers and settings to new sample") + else + TRACE("No valid slice markers could be applied to new sample") + end + + return true +end + +--------------------------------------------------------------------------------------------------- +-- apply the currently selected file in Sononym to the selection in Renoise +function App:do_transfer() + TRACE("App:do_transfer()") + + -- if any of these are true, instrument gets name of sample + local created_instrument = false + local instr_named_after_sample = false + + +if self.prefs.autotransfercreateslot.value then + renoise.song().selected_instrument:insert_sample_at(#renoise.song().selected_instrument.samples+1) + renoise.song().selected_sample_index = #renoise.song().selected_instrument.samples +elseif self.prefs.autotransfercreatenew.value then + renoise.song():insert_instrument_at(renoise.song().selected_instrument_index+1) + renoise.song().selected_instrument_index = renoise.song().selected_instrument_index + 1 +end + + local sample,instr = rns.selected_sample,rns.selected_instrument + + + if not sample then + sample = instr:insert_sample_at(1) + created_instrument = true + else + if (sample.name == instr.name) then + instr_named_after_sample = true + end + end + + if table.is_empty(self.selection_in_sononym) then + return false,"Please define a valid path to the Sononym configuration" + .. "\n(see tool preferences)" + end + + -- combine filename + locationPath + local config_folder,_,__ = cFilesystem.get_path_parts(self.prefs.path_to_config.value) + local folder,_,__ = cFilesystem.get_path_parts(self.selection_in_sononym.locationPath) + + local fpath + if (folder == config_folder) then + -- internal sononym library means filename is relative to the library folder + -- Remove 'sononym.db' from locationPath to get the base folder + local library_base = string.gsub(self.selection_in_sononym.locationPath, "sononym%.db$", "") + fpath = cFilesystem.unixslashes(library_base .. self.selection_in_sononym.filename) + else + -- external path, filename should be combined with folder + fpath = cFilesystem.unixslashes(folder .. self.selection_in_sononym.filename) + end + + TRACE("Constructed file path:", fpath) + + -- If the constructed path doesn't exist, try alternative constructions + if not io.exists(fpath) then + TRACE("Primary path doesn't exist, trying alternatives...") + + -- Try treating the filename as an absolute path + local alt_path1 = cFilesystem.unixslashes(self.selection_in_sononym.filename) + if io.exists(alt_path1) then + TRACE("Found file using filename as absolute path:", alt_path1) + fpath = alt_path1 + else + -- Try combining with the directory containing sononym.db + local alt_path2 = cFilesystem.unixslashes(folder .. "/" .. self.selection_in_sononym.filename) + if io.exists(alt_path2) then + TRACE("Found file using folder + filename with slash:", alt_path2) + fpath = alt_path2 + else + -- Try fuzzy directory matching + local library_base = string.gsub(self.selection_in_sononym.locationPath, "sononym%.db$", "") + if io.exists(library_base) then + local dirs = os.dirnames(library_base) + if dirs and #dirs > 0 then + local target_dir = string.match(self.selection_in_sononym.filename, "([^/]+)") + if target_dir then + TRACE("Fuzzy matching for missing directory:", target_dir) + for _, dir in ipairs(dirs) do + -- Look for directories that contain "tesla" or similar patterns + if string.find(string.lower(dir), "tesla") or + string.find(string.lower(dir), string.lower(target_dir:sub(1, math.min(5, #target_dir)))) then + local remaining_path = string.match(self.selection_in_sononym.filename, "[^/]+/(.+)") + if remaining_path then + local candidate_path = cFilesystem.unixslashes(library_base .. dir .. "/" .. remaining_path) + TRACE("Testing fuzzy match:", candidate_path) + if io.exists(candidate_path) then + TRACE("*** FUZZY MATCH FOUND! Using:", candidate_path) + fpath = candidate_path + break + end + end + end + end + end + end + end + end + end + end + + -- Check if the file exists before attempting to load + if not io.exists(fpath) then + TRACE("File not found at constructed path:", fpath) + TRACE("Original filename:", self.selection_in_sononym.filename) + TRACE("Original locationPath:", self.selection_in_sononym.locationPath) + + -- Debug: Check what's actually in the parent directory + local parent_dir = string.match(fpath, "(.+)/[^/]+$") + if parent_dir and io.exists(parent_dir) then + TRACE("Parent directory exists:", parent_dir) + local files = os.filenames(parent_dir, {"*.*"}) + if files and #files > 0 then + TRACE("Files in parent directory:") + for i, file in ipairs(files) do + if i <= 10 then -- Show first 10 files + TRACE(" -", file) + end + end + if #files > 10 then + TRACE(" ... and", #files - 10, "more files") + end + else + TRACE("No files found in parent directory") + end + + -- Check for directories + local dirs = os.dirnames(parent_dir) + if dirs and #dirs > 0 then + TRACE("Subdirectories in parent:") + for i, dir in ipairs(dirs) do + if i <= 10 then -- Show first 10 directories + TRACE(" -", dir) + end + end + end + else + TRACE("Parent directory does not exist:", parent_dir or "nil") + + -- Check if the library base directory exists + local library_base = string.gsub(self.selection_in_sononym.locationPath, "sononym%.db$", "") + if io.exists(library_base) then + TRACE("Library base directory exists:", library_base) + local dirs = os.dirnames(library_base) + if dirs and #dirs > 0 then + TRACE("Directories in library base:") + for i, dir in ipairs(dirs) do + if i <= 10 then + TRACE(" -", dir) + end + end + + -- Try to find directories that might match the missing one + local target_dir = string.match(self.selection_in_sononym.filename, "([^/]+)") + if target_dir then + TRACE("Looking for directory similar to:", target_dir) + local candidates = {} + for _, dir in ipairs(dirs) do + -- Case insensitive partial match + if string.find(string.lower(dir), string.lower(target_dir:sub(1, 5))) or + string.find(string.lower(target_dir), string.lower(dir:sub(1, 5))) then + table.insert(candidates, dir) + end + end + + if #candidates > 0 then + TRACE("Potential matching directories:") + for _, candidate in ipairs(candidates) do + TRACE(" *", candidate) + -- Try this candidate + local candidate_path = cFilesystem.unixslashes(library_base .. candidate .. "/" .. string.match(self.selection_in_sononym.filename, "[^/]+/(.+)")) + TRACE(" Testing path:", candidate_path) + if io.exists(candidate_path) then + TRACE(" *** FOUND MATCH! Using:", candidate_path) + fpath = candidate_path + break + end + end + else + TRACE("No similar directories found") + end + end + end + else + TRACE("Library base directory does not exist:", library_base) + end + end + + return false,"File does not exist:\n" .. fpath + end + + -- Use slice-aware loading to preserve slice markers and settings + local success,err = pcall(function() + return self:load_sample_with_slice_preservation(sample, fpath) + end) + + if not success then + TRACE("Loading failed with error:", err) + return false,"Failed to load sample:\n"..tostring(err) + end + + if not err then + TRACE("Loading failed - load_sample_with_slice_preservation returned false") + return false,"Failed to load sample: unknown error" + end + + -- update samplename + local folder,filename,ext = cFilesystem.get_path_parts(self.selection_in_sononym.filename) + sample.name = filename + + if created_instrument or instr_named_after_sample then + instr.name = filename + end + + -- display message in status bar / terminal + local msg = "Transferred sample: "..fpath + renoise.app():show_status(msg) + LOG(msg) + + -- Force auto-transfer to kick you to sample editor view on middle frame + if self.live_transfer_observable.value then + renoise.app().window.active_middle_frame = renoise.ApplicationWindow.MIDDLE_FRAME_INSTRUMENT_SAMPLE_EDITOR + TRACE("Auto-transfer: switched to Sample Editor view") + end + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- save the selected sample and launch a similarity search +-- TODO option to save only the *selected range* +-- @return boolean, true or false,string when failed + +function App:do_search() + TRACE("App:do_search()") + + -- check if there's a sample selected first + if not rns.selected_sample then + renoise.app():show_status("There is no sample, doing nothing.") + return false,"There is no sample selected, doing nothing." + end + + -- show important notice the first time + if self.prefs.show_search_warning.value then + local choice = renoise.app():show_prompt("Important notice","" + .."Please make sure that Sononym is running before launching a search" + .."\n(NB: this message is only shown once!)" + ,{"Start Search","Cancel"}) + if (choice == "Cancel") then + return false + else + self.prefs.show_search_warning.value = false + end + end + + local success,err = App.check_path(self.prefs.path_to_exe.value) + if not success then + return false,"Please define a valid path to the Sononym executable" + .."\n(see AppPath in Options, use Detect or Browse.)" + end + + local tmp_path,err = self:_create_temp() + if not tmp_path then + return false,"Unable to Launch Search: " .. err + end + +local path_to_exe=cFilesystem.unixslashes(self.prefs.path_to_exe.value) +local tmp_path=cFilesystem.unixslashes(tmp_path) + + + local path_to_exe = cFilesystem.unixslashes(self.prefs.path_to_exe.value) + local cmd = string.format('"%s" %s',path_to_exe,cFilesystem.unixslashes(tmp_path)) +print (cmd) + local code = os.execute(cmd) + +return true +end + +--------------------------------------------------------------------------------------------------- +-- select a folder and launch Sononym to browse it +function App:do_browse() + TRACE("App:do_browse()") + + local success,err = App.check_path(self.prefs.path_to_exe.value) + if not success then + return false,"Please define a valid path to the Sononym executable" + .."\n(see AppPath in Options, use Detect or Browse.)" + end + + -- Let user select a folder to browse + local folder_path = renoise.app():prompt_for_path("Select Folder to Browse in Sononym") + if folder_path == "" then + return false -- User cancelled + end + + local path_to_exe = cFilesystem.unixslashes(self.prefs.path_to_exe.value) + local browse_path = cFilesystem.unixslashes(folder_path) + + -- Launch Sononym with the folder path to enter browse mode + local cmd = string.format('"%s" "%s"', path_to_exe, browse_path) + TRACE("Launching Sononym browse mode:", cmd) + + local success = pcall(function() + os.execute(cmd) + end) + + if success then + renoise.app():show_status("Launched Sononym in browse mode for: " .. folder_path) + return true + else + return false, "Failed to launch Sononym in browse mode" + end +end + +--------------------------------------------------------------------------------------------------- +-- launch Sononym application + +function App:launch_sononym() + TRACE("App:launch_sononym()") + + local success,err = App.check_path(self.prefs.path_to_exe.value) + if not success then + return false,"Please define a valid path to the Sononym executable" + .."\n(see AppPath in Options, use Detect or Browse.)" + end + + local path_to_exe = cFilesystem.unixslashes(self.prefs.path_to_exe.value) + local cmd = string.format('"%s"',path_to_exe) + print("Launching Sononym: " .. cmd) + local code = os.execute(cmd .. " &") -- run in background + + renoise.app():show_status("Sononym launched.") + return true +end + +--------------------------------------------------------------------------------------------------- +-- save a copy of the selected sample to the temporary folder +-- @return string, temp filename or nil,string if failed + +function App:_create_temp() + + if not rns.selected_sample then + return nil,"No sample is selected." + end + + local buffer = get_sample_buffer(rns.selected_sample) + if not buffer then + return nil,"Sample is empty." + end + local tmp_path = os.tmpname("flac") + local success = buffer:save_as(tmp_path,"flac") + if not success then + return nil,"Failed to save sample." + end + return tmp_path +end + +--------------------------------------------------------------------------------------------------- +function App:detach_sampler() + TRACE("App:detach_sampler()") + + -- First, check if the middle frame is not 5, set it to 5 + if renoise.app().window.active_middle_frame ~= 5 then + renoise.app().window.active_middle_frame = 5 + renoise.app().window.instrument_editor_is_detached = false + renoise.app().window.active_middle_frame = 5 + return + end + + -- If the middle frame is already 5, toggle the instrument editor detachment + if renoise.app().window.instrument_editor_is_detached then + -- Re-attach the instrument editor + + else + -- Detach the instrument editor + renoise.app().window.instrument_editor_is_detached = true + end +end + + +--------------------------------------------------------------------------------------------------- +-- Manual trigger for testing file monitoring (useful for debugging) +function App:test_config_parsing() + TRACE("App:test_config_parsing() - manually parsing config file") + + -- First check if the file exists and show its modification time + local config_path = self.prefs.path_to_config.value + local file_stats = io.stat(config_path) + if file_stats then + TRACE("*** Config file exists, mtime:", file_stats.mtime) + else + TRACE("*** Config file does not exist!") + return + end + + local selection = App.parse_config(config_path) + if selection then + TRACE("*** test parse - found selection:", selection.filename, selection.locationPath) + TRACE("*** test parse - live transfer status:", self.live_transfer_observable.value) + TRACE("*** test parse - monitor active:", self.monitor_active) + self.selection_in_sononym = selection + self.selection_in_sononym_observable:bang() + if self.live_transfer_observable.value then + TRACE("*** test parse - live transfer enabled, transferring...") + local success,err = self:do_transfer() + if not success then + LOG("Test transfer failed:", err) + else + LOG("Test transfer succeeded") + end + else + TRACE("*** test parse - live transfer disabled") + end + else + TRACE("*** test parse - failed to parse config") + end +end + +--------------------------------------------------------------------------------------------------- +-- Debug function to check what's in the query.json file +function App:debug_query_json() + TRACE("App:debug_query_json() - showing current query.json content") + + local config_path = self.prefs.path_to_config.value + if not config_path or config_path == "" then + LOG("No config path set") + return + end + + local file_stats = io.stat(config_path) + if not file_stats then + LOG("Config file does not exist:", config_path) + return + end + + LOG("Config file mtime:", file_stats.mtime) + + -- Read and show the file content + local file = io.open(config_path, "r") + if file then + local content = file:read("*all") + file:close() + LOG("Query.json content:") + LOG(content) + + -- Try to parse it + local selection = App.parse_config(config_path) + if selection then + LOG("Parsed selection:", selection.filename, selection.locationPath) + else + LOG("Failed to parse selection") + end + else + LOG("Could not read config file") + end +end + +--------------------------------------------------------------------------------------------------- +-- Debug detected Sononym versions +function App:debug_versions() + TRACE("App:debug_versions()") + + local versions = App.find_sononym_versions() + local base_path + local debug_info = {} + table.insert(debug_info, "=== Sononym Version Detection ===") + table.insert(debug_info, "Found " .. #versions .. " version(s):") + + if #versions == 0 then + table.insert(debug_info, "No Sononym versions detected") + table.insert(debug_info, "Make sure Sononym is installed and has been run at least once") + end + + local platform = os.platform() + if (platform == "WINDOWS") then + base_path = cFilesystem.get_user_folder() .. "AppData/Roaming/Sononym/" + elseif (platform == "MACINTOSH") then + base_path = cFilesystem.get_user_folder() .. "Library/Application Support/Sononym/" + elseif (platform == "LINUX") then + base_path = cFilesystem.get_user_folder() .. ".config/Sononym/" + end + + table.insert(debug_info, "Checked base path: " .. (base_path or "N/A")) + if base_path then + table.insert(debug_info, "Base path exists: " .. (io.exists(base_path) and "YES" or "NO")) + + if io.exists(base_path) then + local dirs = os.dirnames(base_path) + if dirs and #dirs > 0 then + table.insert(debug_info, "Found " .. #dirs .. " subdirectories:") + for _, dir in ipairs(dirs) do + table.insert(debug_info, " - " .. dir) + end + else + table.insert(debug_info, "No subdirectories found in base path") + end + end + end + + if #versions > 0 then + table.insert(debug_info, "\nDetected versions:") + for i, version_info in ipairs(versions) do + table.insert(debug_info, " " .. i .. ". Version " .. version_info.version) + table.insert(debug_info, " Path: " .. version_info.path) + table.insert(debug_info, " Exists: " .. (io.exists(version_info.path) and "YES" or "NO")) + + if io.exists(version_info.path) then + local file_stat = io.stat(version_info.path) + if file_stat then + table.insert(debug_info, " Size: " .. file_stat.size .. " bytes") + table.insert(debug_info, " Modified: " .. os.date("%Y-%m-%d %H:%M:%S", file_stat.mtime)) + end + end + end + end + + table.insert(debug_info, "\nCurrent config path: " .. self.prefs.path_to_config.value) + table.insert(debug_info, "Current config exists: " .. (io.exists(self.prefs.path_to_config.value) and "YES" or "NO")) + + renoise.app():show_message(table.concat(debug_info, "\n")) +end + +--------------------------------------------------------------------------------------------------- +-- Search for selected sample in Sononym +function App:search_selected_sample() + local success,err = self:do_search() + if not success and err then + renoise.app():show_message(err or "Search failed") + end +end + +--------------------------------------------------------------------------------------------------- +-- Load currently selected sample from Sononym +-- @param show_prompt (boolean) - if true, shows status messages; if false, loads silently +function App:load_selected_sample_from_sononym(show_prompt) + + -- Get the current selection directly from Sononym's JSON using the proper function + local current_selection = App.parse_config(self.prefs.path_to_config.value) + if not current_selection then + renoise.app():show_message("Failed to get Sononym selection.\nMake sure Sononym has a file selected.") + return + end + + local sample_name = string.match(current_selection.filename, "([^/]+)$") or current_selection.filename + local full_path + + -- Check if this is already an absolute path (temp files) or needs to be constructed + if string.match(current_selection.filename, "^/") or string.match(current_selection.filename, "^[A-Za-z]:") then + -- This is already a full absolute path (temp file) + full_path = current_selection.filename + else + -- This is a relative path, construct it using the database location + local clean_path = current_selection.locationPath:gsub("sononym%.db$", "") + full_path = clean_path .. current_selection.filename + end + + TRACE("Selected Sample Full path: " .. full_path) + + local choice = "Load Sample" -- Default to load + + if show_prompt then + choice = renoise.app():show_prompt("Load Sample from Sononym", + "Sample: " .. sample_name .. "\n\nPath: " .. full_path, + {"Load Sample", "Cancel"}) + TRACE("User choice: " .. choice) + else + TRACE("No prompt mode - loading directly") + end + + if choice == "Load Sample" then + -- Load the sample directly using the full path - bypass all the configuration nonsense + if not io.exists(full_path) then + TRACE("Failed to load sample: File does not exist at " .. full_path) + renoise.app():show_message("Failed to load sample:\nFile does not exist:\n" .. full_path) + return + end + + -- Load directly into Renoise - create new instrument + local success, err = pcall(function() + -- Create a new instrument using the correct API + rns:insert_instrument_at(rns.selected_instrument_index + 1) + rns.selected_instrument_index = rns.selected_instrument_index + 1 + + -- Insert a new sample slot in the new instrument + rns.selected_instrument:insert_sample_at(1) + rns.selected_sample_index = 1 + + -- Load the sample into the new sample slot + local sample = rns.selected_instrument.samples[1] + sample.sample_buffer:load_from(full_path) + + -- Set sample name to the filename (without path) + sample.name = sample_name + + -- Set instrument name to the filename as well for easy identification + rns.selected_instrument.name = sample_name + end) + + if not success then + TRACE("Failed to load sample: " .. tostring(err)) + renoise.app():show_message("Failed to load sample:\n" .. tostring(err)) + else + TRACE("Sample loaded: " .. sample_name) + renoise.app():show_status("Sample loaded: " .. sample_name) + end + end +end + +--------------------------------------------------------------------------------------------------- +-- Static methods +--------------------------------------------------------------------------------------------------- +-- Parse sononym configuration file (query.json) to find currently selected path +-- @param path (string) +-- @return table { +-- filename (string) +-- locationPath (string) +--} +-- or nil,error message (string) + +function App.parse_config(path) + TRACE("App.parse_config(path)") + + -- return error if no path is supplied + if not path or (path == "") then + return nil,"No path is supplied" + end + + -- load the config file + local fhandle = io.open(path,"r") + if not fhandle then + return nil, "ERROR: Failed to open file handle" + end + + local str_json = fhandle:read("*a") + fhandle:close() + + -- parse the string + local first,last = nil,nil + local offset = string.find(str_json,'"selectedFile"') + if not offset then + return nil, "ERROR: selectedFile not found in config" + end + str_json = string.sub(str_json,offset) + + first,last = string.find(str_json,'"filename":%C*') + if not first or not last then + return nil, "ERROR: filename not found in config" + end + local filename = cFilesystem.unixslashes(string.sub(str_json,first+13,last-2)) + + first,last = string.find(str_json,'"locationPath":%C*') + if not first or not last then + return nil, "ERROR: locationPath not found in config" + end + local locationPath = cFilesystem.unixslashes(string.sub(str_json,first+17,last-1)) + + return { + filename = filename, + locationPath = locationPath, + } + +end + +--------------------------------------------------------------------------------------------------- +-- check if path is valid and existing +-- @return boolean + +function App.check_path(str_path) + TRACE("App.check_path(str_path)",str_path) + + if not str_path or (str_path == "") then + return false,"No path specified" + end + + if not io.exists(str_path) then + return false,"Path does not exist" + end + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- attempt to resolve the location of the Sononym executable +function App.guess_path_to_exe() + TRACE("App.guess_path_to_exe()") + + local platform = os.platform() + if (platform == "WINDOWS") then + -- spawn terminal to obtain windows environment variable + local cmd = "echo %PROGRAMFILES%" + local f = assert(io.popen(cmd, 'r')) + local s = assert(f:read('*a')) + f:close() + return cFilesystem.unixslashes(cString.trim(s).."/Sononym/Sononym.exe") + elseif (platform == "MACINTOSH") then + return "/Applications/Sononym.app/Contents/MacOS/Sononym" + elseif (platform == "LINUX") then + -- First try the standard location + local standard_path = "/usr/bin/sononym" + LOG("Linux - checking standard path:", standard_path) + if io.exists(standard_path) then + LOG("Linux - found Sononym at standard path:", standard_path) + return standard_path + end + + -- If not found, use 'which' to search PATH + LOG("Linux - standard path not found, trying 'which' command") + + -- First test if io.popen works at all in Renoise + LOG("Linux - testing io.popen functionality") + local test_success, test_handle = pcall(io.popen, "echo test") + if test_success and test_handle then + local test_result = test_handle:read("*line") + test_handle:close() + LOG("Linux - io.popen test result:", test_result or "(empty)") + if not test_result or cString.trim(test_result) ~= "test" then + LOG("Linux - io.popen is not working properly in Renoise environment") + end + else + LOG("Linux - io.popen is not available or restricted in Renoise environment") + end + + -- Try multiple approaches to find the executable + local search_commands = { + "which sononym", + "command -v sononym", + "type -p sononym" + } + + for _, cmd in ipairs(search_commands) do + LOG("Linux - trying command:", cmd) + local success, handle = pcall(io.popen, cmd) + if success and handle then + local result = handle:read("*line") + handle:close() + + -- Trim whitespace from result + if result then + result = cString.trim(result) + end + + LOG("Linux - command result:", result or "(empty)") + if result and result ~= "" then + LOG("Linux - checking if path exists:", result) + if io.exists(result) then + LOG("Found Sononym via '" .. cmd .. "' at:", result) + return cFilesystem.unixslashes(result) + else + LOG("Linux - path returned by command doesn't exist:", result) + end + end + else + LOG("Linux - failed to execute command:", cmd) + end + end + + LOG("Linux - all search commands failed or returned no results") + + -- Try some common alternative locations + local user = os.getenv("USER") or "user" + local home = os.getenv("HOME") or ("/home/" .. user) + local alt_paths = { + "/usr/local/bin/sononym", + "/opt/sononym/sononym", + "/opt/Sononym/sononym", + home .. "/.local/bin/sononym", + home .. "/bin/sononym", + home .. "/Applications/Sononym/sononym", + "/snap/bin/sononym", + "/var/lib/flatpak/exports/bin/sononym" + } + + for _, alt_path in ipairs(alt_paths) do + LOG("Linux - checking alternative path:", alt_path) + if io.exists(alt_path) then + LOG("Linux - found Sononym at alternative path:", alt_path) + return alt_path + end + end + + LOG("Linux - Sononym executable not found in any standard locations") + -- Fallback to standard path (even if it doesn't exist, for user reference) + return standard_path + end + +end + +--------------------------------------------------------------------------------------------------- +-- find all available Sononym versions and their query.json files +function App.find_sononym_versions() + TRACE("App.find_sononym_versions()") + + local platform = os.platform() + local base_path + + if (platform == "WINDOWS") then + base_path = cFilesystem.get_user_folder() .. "AppData/Roaming/Sononym/" + elseif (platform == "MACINTOSH") then + base_path = cFilesystem.get_user_folder() .. "Library/Application Support/Sononym/" + elseif (platform == "LINUX") then + local user_folder = cFilesystem.get_user_folder() + -- Ensure user_folder ends with "/" for proper path construction + if not string.match(user_folder, "/$") then + user_folder = user_folder .. "/" + end + base_path = user_folder .. ".config/Sononym/" + LOG("Linux - User folder:", user_folder) + LOG("Linux - Base path:", base_path) + else + return {} + end + + local versions = {} + + -- Check if base Sononym directory exists + if not io.exists(base_path) then + LOG("Sononym base directory not found:", base_path) + return versions + end + + LOG("Scanning for Sononym versions in:", base_path) + + -- Look for version directories (like 1.5.5, 1.5.6, etc.) + local dirs = os.dirnames(base_path) + if dirs then + LOG("Found", #dirs, "directories in", base_path) + for _, dir in ipairs(dirs) do + LOG("Checking directory:", dir) + -- Check if this looks like a version number (starts with digit and contains dots) + -- This will match patterns like: 1.5.5, 1.5.6, 2.0.0, 1.6.0-beta, etc. + if string.match(dir, "^%d+%.%d+") then + local query_path = base_path .. dir .. "/query.json" + LOG("Testing query.json path:", query_path) + if io.exists(query_path) then + table.insert(versions, { + version = dir, + path = query_path + }) + LOG("Found Sononym version:", dir, "with query.json at", query_path) + else + LOG("query.json not found at:", query_path) + end + else + LOG("Directory", dir, "doesn't match version pattern") + end + end + else + LOG("No directories found in", base_path) + end + + -- Sort versions (newest first) using proper version comparison + table.sort(versions, function(a, b) + -- Split version strings into numbers for proper comparison + local a_parts = {} + local b_parts = {} + + for num in string.gmatch(a.version, "%d+") do + table.insert(a_parts, tonumber(num)) + end + + for num in string.gmatch(b.version, "%d+") do + table.insert(b_parts, tonumber(num)) + end + + -- Compare version parts (major, minor, patch) + for i = 1, math.max(#a_parts, #b_parts) do + local a_part = a_parts[i] or 0 + local b_part = b_parts[i] or 0 + + if a_part ~= b_part then + return a_part > b_part -- Higher version number comes first + end + end + + return false -- Equal versions + end) + + return versions +end + +--------------------------------------------------------------------------------------------------- +-- attempt to resolve the location of 'query.json' with version detection +function App.guess_path_to_config() + TRACE("App.guess_path_to_config()") + + local versions = App.find_sononym_versions() + LOG("*** guess_path_to_config: found", #versions, "versions") + + if #versions == 0 then + -- No versions found at all + LOG("No Sononym versions detected - check if Sononym is installed") + + -- On Linux, provide a helpful fallback suggestion + local platform = os.platform() + if (platform == "LINUX") then + local user_folder = cFilesystem.get_user_folder() + if not string.match(user_folder, "/$") then + user_folder = user_folder .. "/" + end + local fallback_path = user_folder .. ".config/Sononym/" + LOG("Linux fallback: try looking in", fallback_path) + -- Check if the base directory exists but no versions were found + if io.exists(fallback_path) then + LOG("Base Sononym directory exists but no version directories found") + -- Look for any query.json files in subdirectories + local dirs = os.dirnames(fallback_path) + if dirs then + for _, dir in ipairs(dirs) do + local query_path = fallback_path .. dir .. "/query.json" + LOG("Fallback - checking:", query_path) + if io.exists(query_path) then + LOG("Found query.json in non-standard directory:", dir, "-> returning:", query_path) + return query_path + end + end + end + end + end + + LOG("*** guess_path_to_config: returning nil (no versions found)") + return nil + else + -- Always return newest version - UI will handle multiple version selection + local newest_version = versions[1] + LOG("Auto-selected newest Sononym version:", newest_version.version, "at", newest_version.path) + LOG("*** guess_path_to_config: returning path:", newest_version.path) + return newest_version.path + end +end + + + +--local prefs = AppPrefs() +local prefs = AppPrefs() +renoise.tool().preferences = prefs + + + +function OpenConfigPath() + +--print (prefs.path_to_config) +local config_path = renoise.tool().preferences.path_to_config.value +local directory_path = config_path:match("(.*/)") +oprint(os.platform()) +oprint(directory_path) +oprint(config_path) + local command +local os_name = os.platform() + + if os_name == "WINDOWS" then command = 'start "" "' .. directory_path .. '"' + elseif os_name == "MACINTOSH" then command = 'open "' .. directory_path .. '"' + else os_name = 'xdg-open "' .. directory_path .. '"' end + os.execute(command) + + +end + + + +function flip_a_coin(file_path) + -- Open the file containing the JSON + local file = io.open(file_path, "r") + + if not file then + renoise.app():show_status("Error: Cannot open file at " .. file_path) + return nil, nil + end + + -- Read the entire file content as a string + local queryjsonvariable = file:read("*all") + file:close() + + -- NOTE: Sononym's query.json only contains the currently selected file, + -- not all search results. For true randomness, user needs to have + -- search results displayed in Sononym first. + + -- Extract the selected filename from the JSON + local selected_filename = queryjsonvariable:match('"filename"%s*:%s*"([^"]+)"') + if not selected_filename then + renoise.app():show_status("Error: No filename found in Sononym selection.") + return nil, nil + end + + -- Extract just the filename without path for display + local display_name = string.match(selected_filename, "([^/]+)$") or selected_filename + + -- Check if this is already an absolute path (temp files) or needs to be constructed + local full_path + if string.match(selected_filename, "^/") or string.match(selected_filename, "^[A-Za-z]:") then + -- This is already a full absolute path (temp file) + full_path = selected_filename + else + -- This is a relative path, construct it using the database location + local selectedLocationPath = queryjsonvariable:match('"selectedLocationPath"%s*:%s*"([^"]+)"') + if not selectedLocationPath then + renoise.app():show_status("Error: selectedLocationPath not found.") + return nil, nil + end + + -- Remove 'sononym.db' from selectedLocationPath to get base path + local clean_path = selectedLocationPath:gsub("sononym%.db$", "") + full_path = clean_path .. selected_filename + end + + return display_name, full_path +end + +-- flip_a_coin(renoise.tool().preferences.path_to_config.value) -- Removed auto-call \ No newline at end of file diff --git a/Tools/com.renoise.Sononymph.xrnx/AppUI.lua b/Tools/com.renoise.Sononymph.xrnx/AppUI.lua new file mode 100644 index 00000000..16b3e037 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/AppUI.lua @@ -0,0 +1,605 @@ +class "AppUI" + +AppUI.LABEL_W = 85 +AppUI.INPUT_W = 350 +AppUI.BUTTON_W = 130 +AppUI.BUTTON_W2 = 150 +AppUI.DIALOG_W = AppUI.LABEL_W + AppUI.INPUT_W + AppUI.BUTTON_W +AppUI.RENOISE_PLACEHOLDER = "No sample selected" +AppUI.SONONYM_PLACEHOLDER = "-" + +--------------------------------------------------------------------------------------------------- +function AppUI:__init(...) + local args = cLib.unpack_args(...) + assert(type(args.owner)=="App","Expected 'owner' to be an instance of App") + + -- App + self.owner = args.owner + + -- Config paths for dropdown + self.config_paths = {} + + -- Dialog management + self.dialog = nil + self.dialog_title = args.dialog_title or "Sononymph" + self.dialog_content = nil + self.vb = nil + + -- Toggle button state + self.prefs_toggle_enabled = false + + + + -- Update flag + self.update_requested = false + + -- notifications ------------------------------ + + self.owner.monitor_active_observable:add_notifier(function() + self.update_requested = true + end) + + self.owner.prefs.path_to_exe:add_notifier(function() + self.update_requested = true + end) + + self.owner.prefs.path_to_config:add_notifier(function() + self.update_requested = true + end) + + self.owner.prefs.show_prefs:add_notifier(function() + self.update_requested = true + self.prefs_toggle_enabled = self.owner.prefs.show_prefs.value + end) + + self.owner.selection_in_sononym_observable:add_notifier(function() + self.update_requested = true + end) + + self.owner.live_transfer_observable:add_notifier(function() + self.update_requested = true + end) + + self.owner.paths_are_valid_observable:add_notifier(function() + self.update_requested = true + end) + + -- initialize + renoise.tool().app_idle_observable:add_notifier(self,self.on_idle) + renoise.tool().app_new_document_observable:add_notifier(function() + self:attach_to_song() + end) + + self:attach_to_song() + + -- Initialize toggle state + self.prefs_toggle_enabled = self.owner.prefs.show_prefs.value + +end + +--------------------------------------------------------------------------------------------------- +-- Handle keyboard shortcuts in the dialog +function AppUI:dialog_keyhandler(dialog, key) + TRACE("AppUI:dialog_keyhandler", key.name, key.modifiers) + + -- ESC - Close dialog + if (key.name == "esc") then + self:close() + return key -- Key handled, don't pass to Renoise + else return key + end +end + +--------------------------------------------------------------------------------------------------- +-- create/re-use existing dialog +function AppUI:show() + TRACE("AppUI:show()") + + if not self.dialog or not self.dialog.visible then + if not self.dialog_content then + self.dialog_content = self:create_dialog() + end + self.dialog = renoise.app():show_custom_dialog( + self.dialog_title, self.dialog_content, function(dialog, key) + return self:dialog_keyhandler(dialog, key) + end) + else + self.dialog:show() + end + + if not self.owner.paths_are_valid then + local app_path = self.owner.prefs.path_to_exe.value + local config_path = self.owner.prefs.path_to_config.value + + -- First-time install: both paths are empty, auto-detect silently + if (app_path == "" and config_path == "") then + renoise.app():show_status("First time setup - auto-detecting Sononym paths...") + self.owner:autoconfigure() + else + -- Paths exist but are invalid, show prompt + local choice = renoise.app():show_prompt("Configure Sononymph", + "Sononymph needs to be configured. Do you want to automatically detect" + .."\nthe appropriate paths for the Sononym app and its configuration?", + {"Detect Automatically","Enter Manually"}) + if (choice == "Detect Automatically") then + self.owner:autoconfigure() + end + end + end + + self.update_requested = true + return true +end + +--------------------------------------------------------------------------------------------------- +function AppUI:close() + TRACE("AppUI:close()") + + if self.dialog and self.dialog.visible then + self.dialog:close() + end +end + +--------------------------------------------------------------------------------------------------- +function AppUI:dialog_is_visible() + return self.dialog and self.dialog.visible or false +end + +--------------------------------------------------------------------------------------------------- +-- Create dialog content using regular Renoise ViewBuilder +function AppUI:create_dialog() + self.vb = renoise.ViewBuilder() -- Reset the ViewBuilder + local vb = self.vb + + return vb:column{ + margin = 4, + spacing = 4, + vb:column{ + margin = 6, + spacing = 4, + style = "group", + vb:row{ + vb:text{ + text = "Renoise",style="strong",font="bold", + width = AppUI.LABEL_W - 18, + }, + vb:button{ + bitmap = "./source/icons/detach.bmp", + tooltip="Detach/Reattach Sample Editor and show.", + notifier = function() + self.owner:detach_sampler() + end + }, + vb:row{ + style = "plain", + vb:text{ + id = "sample_name_renoise",style="strong",font="bold", + text = AppUI.RENOISE_PLACEHOLDER, + width = AppUI.INPUT_W, + }, + }, + vb:button{ + id = "bt_search", + text = "Search in Sononym", + tooltip = "Click to launch a similarity search for this sample", + width = AppUI.BUTTON_W2, + notifier = function() + local success,err = self.owner:do_search() + if not success then + renoise.app():show_message(err or "Search failed") + end + end + }, + vb:button{ + id = "bt_browse", + text = "Browse Path in Sononym", + tooltip = "Select a folder and browse it in Sononym", + width = AppUI.BUTTON_W2, + notifier = function() + local success,err = self.owner:do_browse() + if not success then + renoise.app():show_message(err or "Browse failed") + end + end + }, + }, + vb:row{ + vb:column{ + vb:text{ + id = "label_filename_sononym", + text = "Sononym",style="strong",font="bold", + width = AppUI.LABEL_W, + }, + }, + vb:column{ + vb:row{ + vb:text{ + id = "filename_sononym", + text = AppUI.SONONYM_PLACEHOLDER, + width = AppUI.INPUT_W,style="strong",font="bold", + }, + }, + + }, + vb:column{ + vb:row{ + vb:button{ + id = "bt_transfer", + text = "Transfer from Sononym", + tooltip = "Click to transfer the selected sample from Sononym", + width = AppUI.BUTTON_W2*2, + notifier = function() + local success,err = self.owner:do_transfer() + if not success then + renoise.app():show_message(err or "Transfer failed") + end + renoise.app().window.active_middle_frame=renoise.app().window.active_middle_frame + renoise.app():show_status("Sample successfully loaded.") + end + }, + }, + }, + }, + vb:row{ + vb:space{ + width = AppUI.LABEL_W, + }, + vb:checkbox{ + id = "cb_transfer_toggle", + notifier = function() + local success,err = self.owner:toggle_live_transfer() + if not success and err then + renoise.app():show_message(err or "Auto-transfer toggle failed") + end + end + }, + vb:text{ + text = "Auto-transfer",style="strong",font="bold", + }, + vb:checkbox{ + bind = self.owner.prefs.autotransfercreatenew, + }, + vb:text{ + text = "New Instrument",style="strong",font="bold", + }, + vb:checkbox{ + bind = self.owner.prefs.autotransfercreateslot, + }, + vb:text{ + text = "New Sample Slot",style="strong",font="bold", + }, + }, + vb:row{ + vb:button{ + id = "prefs_toggle", + text = "▴", -- Start with collapsed state + width = 22, + notifier = function() + self.owner.prefs.show_prefs.value = not self.owner.prefs.show_prefs.value + self.prefs_toggle_enabled = self.owner.prefs.show_prefs.value + -- Update button text immediately + local btn = vb.views["prefs_toggle"] + btn.text = self.prefs_toggle_enabled and "▾" or "▴" + end + }, + vb:text{ + text = "Options",style = "strong",font = "bold",width=AppUI.LABEL_W-22, + }, + vb:text{ + id = "location_path_sononym", + text = "[Library: ",style="strong",font="bold", + }, + }, + }, + vb:column{ + style = "group", + id = "preferences_content", + margin = 6, + vb:space{ + width = AppUI.DIALOG_W, + }, + vb:column{ + vb:row{ + vb:text{ + text = "AppPath",style="strong",font="bold", + width = AppUI.LABEL_W, + }, + vb:row{ + style = "plain", + vb:textfield{ + id = "path_to_exe", + text = "", + width = AppUI.INPUT_W, + notifier = function(txt) + local success,err = self.owner:set_path_to_exe(txt) + if not success then + renoise.app():show_warning(err) + end + end + }, + }, + vb:row{ + vb:button{ + text = "Detect", + width = (2 * AppUI.BUTTON_W2) / 3, + notifier = function() + local choice = renoise.app():show_prompt("Auto-detect path", + "This will attempt to auto-detect the path to the Sononym app. " + .."\nAre you sure you want to do this?", + {"OK","Cancel"}) + if (choice == "OK") then + local success,err = self.owner:set_path_to_exe(App.guess_path_to_exe()) + if not success then + if err then + renoise.app():show_warning(err) + end + end + end + end + }, + vb:button{ + text = "Browse...", + width = (2 * AppUI.BUTTON_W2) / 3, + notifier = function() + self.owner:pick_path_to_exe() + end + }, + vb:button{ + text = "Launch", + width = (2 * AppUI.BUTTON_W2) / 3, + notifier = function() + local success,err = self.owner:launch_sononym() + if not success and err then + renoise.app():show_warning(err) + end + end + }, + }, + }, + vb:row{ + vb:text{ + text = "ConfigPath",style="strong",font="bold", + width = AppUI.LABEL_W, + }, + vb:column{ + -- Container to hold both the textfield and popup + vb:textfield{ + id = "path_to_config", + text = "", + width = AppUI.INPUT_W, + notifier = function(txt) + local success, err = self.owner:set_path_to_config(txt) + if not success then + renoise.app():show_warning(err) + end + end + }, + vb:popup{ + id = "config_path_popup", + width = AppUI.INPUT_W, + visible = false, -- Initially hidden + items = {}, + notifier = function(index) + local selected_version = self.config_paths[index] + if selected_version then + local success, err = self.owner:set_path_to_config(selected_version.path) + if not success then + renoise.app():show_warning(err) + end + -- Update the textfield with the selected path + vb.views["path_to_config"].text = selected_version.path + -- Hide the popup and show the textfield + vb.views["config_path_popup"].visible = false + vb.views["path_to_config"].visible = true + end + end + } + }, + vb:row{ + vb:button{ + text = "Detect", + width = (2 * AppUI.BUTTON_W2) / 3, + notifier = function() + local choice = renoise.app():show_prompt("Auto-detect path", + "This will scan for available Sononym configurations.\nAre you sure you want to do this?", + {"OK","Cancel"}) + if (choice == "OK") then + -- Get the list of available configurations + local versions = App.find_sononym_versions() + if #versions == 0 then + renoise.app():show_warning("No Sononym versions found.") + elseif #versions == 1 then + -- Only one version found - set it directly with full path + local version_info = versions[1] + local success, err = self.owner:set_path_to_config(version_info.path) + if success then + renoise.app():show_status("ConfigPath set to: " .. version_info.path) + -- Update the textfield display + vb.views["path_to_config"].text = version_info.path + else + renoise.app():show_warning(err or "Failed to set ConfigPath") + end + else + -- Multiple versions found - show dropdown for selection + self.config_paths = versions + + -- Build dropdown items + local dropdown_items = {} + for i, version_info in ipairs(versions) do + table.insert(dropdown_items, "Sononym " .. version_info.version .. " (" .. version_info.path .. ")") + end + + -- Populate the popup menu + vb.views["config_path_popup"].items = dropdown_items + -- Show the popup and hide the textfield + vb.views["config_path_popup"].visible = true + vb.views["path_to_config"].visible = false + renoise.app():show_status("Multiple versions found - please select one") + end + end + end + }, + vb:button{ + text = "Browse...", + width = (2 * AppUI.BUTTON_W2) / 3, + notifier = function() + self.owner:pick_path_to_config() + end + }, + vb:button{ + text = "Open Path", + width = (2 * AppUI.BUTTON_W2) / 3, + notifier = function() + OpenConfigPath() + end + }, + }, + }, + vb:row{ + vb:text{ + text = "Status ",style="strong",font="bold", + width = AppUI.LABEL_W, + }, + vb:text{ + text = "",style="strong",font="bold", + id = "txt_tool_status", + }, + }, + }, + vb:row{ + vb:button{ + text = "Full Sononym Documentation", + width = AppUI.BUTTON_W, + notifier = function() + renoise.app():open_url("https://www.sononym.net/docs/") + end + }, + vb:button{ + text = "Sononymph Forum Thread", + width = AppUI.BUTTON_W, + notifier = function() + renoise.app():open_url("https://forum.renoise.com/t/new-tool-3-4-sononymph-with-paketti-improvements-renoise-sononym-integration/76581") + end + }, + vb:checkbox{ + bind = self.owner.prefs.autostart, + }, + vb:text{ + text = "Autostart", + style = "strong", + font = "bold", + width = 60, + }, + }, + }, + } + +end + +--------------------------------------------------------------------------------------------------- +-- Class methods +--------------------------------------------------------------------------------------------------- + +function AppUI:update() + if not self.dialog or not self.dialog.visible then + return + end + + local ctrl + local vb = self.vb + + local buffer = rns.selected_sample + and get_sample_buffer(rns.selected_sample) + local samplename = rns.selected_sample + and get_display_name(rns.selected_sample,rns.selected_sample_index) + if samplename and not buffer then + samplename = samplename .. " (empty)" + end + ctrl = vb.views["sample_name_renoise"] + ctrl.text = samplename or AppUI.RENOISE_PLACEHOLDER + ctrl.tooltip = ctrl.text + ctrl.width = AppUI.INPUT_W + + local filename = self.owner.selection_in_sononym and self.owner.selection_in_sononym.filename + ctrl = vb.views["filename_sononym"] + ctrl.text = filename or AppUI.SONONYM_PLACEHOLDER + ctrl.tooltip = ctrl.text + ctrl.width = AppUI.INPUT_W + + local location_path = self.owner.selection_in_sononym.locationPath + and cFilesystem.get_path_parts(self.owner.selection_in_sononym.locationPath) + ctrl = vb.views["location_path_sononym"] + ctrl.text = "[Library: ".. (location_path or AppUI.SONONYM_PLACEHOLDER) .. "]" + ctrl.tooltip = ctrl.text + ctrl.width = AppUI.INPUT_W + + local path_to_exe = self.owner.prefs.path_to_exe.value + ctrl = vb.views["path_to_exe"] + ctrl.text = path_to_exe + ctrl.tooltip = ctrl.text + ctrl.width = AppUI.INPUT_W + + local path_to_config = self.owner.prefs.path_to_config.value + ctrl = vb.views["path_to_config"] + ctrl.text = path_to_config + ctrl.tooltip = ctrl.text + ctrl.width = AppUI.INPUT_W + + ctrl = vb.views["bt_transfer"] + ctrl.active = not self.owner.live_transfer_observable.value + + -- Update the auto-transfer checkbox to reflect current state + ctrl = vb.views["cb_transfer_toggle"] + if ctrl then + ctrl.value = self.owner.live_transfer_observable.value + end + + -- Update preferences section visibility + ctrl = vb.views["preferences_content"] + ctrl.visible = self.owner.prefs.show_prefs.value + + -- Update toggle button text + ctrl = vb.views["prefs_toggle"] + if ctrl then + ctrl.text = self.prefs_toggle_enabled and "▾" or "▴" + end + + ctrl = self.vb.views["txt_tool_status"] + local paths_are_valid = self.owner.paths_are_valid_observable.value + local monitor_active = self.owner.monitor_active_observable.value + ctrl.text = (paths_are_valid and monitor_active) + and "✔ Monitoring for changes..." + or "⚠ Invalid path: "..self.owner.invalid_path_observable.value + +end + +--------------------------------------------------------------------------------------------------- +--- handle idle notifications + +function AppUI:on_idle() + + if self.update_requested then + self.update_requested = false + self:update() + end + + local is_visible = self:dialog_is_visible() + if not is_visible and self.owner.monitor_active then + self.owner:stop_monitoring() + elseif is_visible and not self.owner.monitor_active then + self.owner:start_monitoring() + end + +end + + + +--------------------------------------------------------------------------------------------------- +-- invoked when a new document becomes available +function AppUI:attach_to_song() + + rns.selected_sample_observable:add_notifier(function() + self.update_requested = true + end) + +end \ No newline at end of file diff --git a/Tools/com.renoise.Sononymph.xrnx/README.md b/Tools/com.renoise.Sononymph.xrnx/README.md index a682b77d..fdb89f36 100644 --- a/Tools/com.renoise.Sononymph.xrnx/README.md +++ b/Tools/com.renoise.Sononymph.xrnx/README.md @@ -1,67 +1,67 @@ -# Sononym(ph) - -![Splash Image](docs/splash-large.png) - -This tool is an integration of the Sononym sample browser into Renoise. Use it to browse for samples using the features in Sononym while listening to the result in the context of your Renoise project. - -## Features at a glance - -* Launch similarity search from the selected sample (Renoise → Sononym) -* Transfer samples from Sononym → Renoise -* Replace samples in Renoise while browsing in Sononym (auto-transfer) - -## Links - -* Youtube demonstration: TODO -* Download from tool page: http://renoise.com/tools/sononymph -* Discuss in Renoise forum: http://forum.renoise.com/index.php/topic/52097-new-tool-31-sononym-integration-preview/ -* Check for / report issues: https://github.com/renoise/xrnx/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sononymph - -## Quickstart - -1. Launch Renoise and start the tool from the Renoise tools menu. -Note that the tool needs to be configured when you launch it for the first time. -Follow the instructions on screen - see also the [preferences](#preferences) section below. -2. Launch Sononym - **IMPORTANT** ([more info](#search-in-sononym)). -3. Renoise: Select a sample and hit 'Search in Sononym' -4. Sononym: Browse around until you find something good -5. Renoise: Hit 'Transfer' to import the selected sample in Sononym - - -## The user interface - -![Screenshot](docs/screenshot.png) - -_#_ |Description -----|---------------- -1 | Displays the currently selected sample in Renoise.
Click the button to detach the instrument editor. -2 | Displays the currently selected sample in Sononym -3 | Displays the Sononym library that the sample belongs to -4 | [Launch a similarity search](#search-in-sononym) on the selected sample (1) -5 | Transfer the selected sample (2) from Sononym to Renoise -6 | Enable or disable the [automatic transfer mode](#auto-transfer) -7 | Decides if the tool starts automatically when Renoise is launched -8 | Open a dialog containing user instructions -9 | Specifies paths to [Sononym executable + configuration](#preferences) -10 | Click to auto-detect the Sononym paths (9) -11 | Click to open a file system dialog to set paths (9) -12 | Current tool status: "Monitoring...", "Invalid path" - -## Additional notes - -### Search in Sononym -Click this button to launch a similarity search in Sononym using the currently selected sample in Renoise as the source. - -**IMPORTANT: Sononym should be running _before_ launching a search** - -otherwise the Sononym process might lock Renoise. If you do this by -accident, simply close the Sononym window and start Sononym -from its usual place (Start Menu, Dock, etc). - -### Auto-transfer -Enable this to automatically replace samples in Renoise while browsing in Sononym. The mode detects when the selection in Sononym has changed, and will automatically perform a 'transfer'. - -### Preferences - -#### Path to exe / path to config -The location of these paths depend on the operating system. The Sononym documentation specifies the typical locations, and how you can find them yourself: https://www.sononym.net/docs/installation/overview/) - +# Sononym(ph) + +![Splash Image](docs/splash-large.png) + +This tool is an integration of the Sononym sample browser into Renoise. Use it to browse for samples using the features in Sononym while listening to the result in the context of your Renoise project. + +## Features at a glance + +* Launch similarity search from the selected sample (Renoise → Sononym) +* Transfer samples from Sononym → Renoise +* Replace samples in Renoise while browsing in Sononym (auto-transfer) + +## Links + +* Youtube demonstration: TODO +* Download from tool page: http://renoise.com/tools/sononymph +* Discuss in Renoise forum: https://forum.renoise.com/t/new-tool-3-1-sononym-integration-preview/49660 +* Check for / report issues: https://github.com/renoise/xrnx/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sononymph + +## Quickstart + +1. Launch Renoise and start the tool from the Renoise tools menu. +Note that the tool needs to be configured when you launch it for the first time. +Follow the instructions on screen - see also the [preferences](#preferences) section below. +2. Launch Sononym - **IMPORTANT** ([more info](#search-in-sononym)). +3. Renoise: Select a sample and hit 'Search in Sononym' +4. Sononym: Browse around until you find something good +5. Renoise: Hit 'Transfer' to import the selected sample in Sononym + + +## The user interface + +![Screenshot](docs/screenshot.png) + +_#_ |Description +----|---------------- +1 | Displays the currently selected sample in Renoise.
Click the button to detach the instrument editor. +2 | Displays the currently selected sample in Sononym +3 | Displays the Sononym library that the sample belongs to +4 | [Launch a similarity search](#search-in-sononym) on the selected sample (1) +5 | Transfer the selected sample (2) from Sononym to Renoise +6 | Enable or disable the [automatic transfer mode](#auto-transfer) +7 | Decides if the tool starts automatically when Renoise is launched +8 | Open a dialog containing user instructions +9 | Specifies paths to [Sononym executable + configuration](#preferences) +10 | Click to auto-detect the Sononym paths (9) +11 | Click to open a file system dialog to set paths (9) +12 | Current tool status: "Monitoring...", "Invalid path" + +## Additional notes + +### Search in Sononym +Click this button to launch a similarity search in Sononym using the currently selected sample in Renoise as the source. + +**IMPORTANT: Sononym should be running _before_ launching a search** - +otherwise the Sononym process might lock Renoise. If you do this by +accident, simply close the Sononym window and start Sononym +from its usual place (Start Menu, Dock, etc). + +### Auto-transfer +Enable this to automatically replace samples in Renoise while browsing in Sononym. The mode detects when the selection in Sononym has changed, and will automatically perform a 'transfer'. + +### Preferences + +#### Path to exe / path to config +The location of these paths depend on the operating system. The Sononym documentation specifies the typical locations, and how you can find them yourself: https://www.sononym.net/docs/installation/overview/) + diff --git a/Tools/com.renoise.Sononymph.xrnx/changelog.md b/Tools/com.renoise.Sononymph.xrnx/changelog.md new file mode 100644 index 00000000..9c24219c --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/changelog.md @@ -0,0 +1,64 @@ +# Changelog + +## 0.91 + +### Major Changes +- Removed vLib and xLib dependencies - dialog now uses native Renoise ViewBuilder to show an almost identical dialog. (Cleaned up 101 unused library files, making the tool much lighter) +- About dialog removed - moved documentation and forum buttons to main dialog +- Removed duplicate "Open ConfigPath" button for cleaner UI +- Made "Autostart" text bold to match other labels +- Fixed startup crash caused by DocumentNode constructor error +- Removed AppPrefs.lua from the codebase, as it's consolidated into main.lua. + +## 0.9 + +### Small tweaks and improvements by Esa Ruoho (Lackluster) +- Added menu entries in 6 different contexts (Instrument Box, Sample Editor, Sample Navigator, Main Menu) +- Auto-transfer can now create new instruments or sample slots instead of just overwriting +- Added functions for loading samples from Sononym with or without prompts +- Added folder browsing in Sononym and direct app launching +- Smart version detection across Windows, macOS, and Linux + +### MIDI Mappings & Keybindings +- `Sononymph:Toggle Auto-Transfer [Trigger]` MIDI mapping and keybinding +- `Sononymph:Load Selected Sample from Sononym` with prompt and no-prompt versions (MIDI + keybindings) +- `Global:Sononymph:Open Sononymph Dialog...` keybinding + +### Interface Improvements +- Launch button to start Sononym directly +- Browse Path button to select folders in Sononym +- Open Path button on ConfigPath row for easy file access +- Dropdown for Sononym version selection instead of using the first one found +- Auto-transfer preserves sample slices and automatically switches to Sample Editor +- First-time setup detects paths automatically without nagging the user + +### Linux Support +- Proper ConfigPath detection for version-specific files like `/home/user/.config/Sononym/1.5.6/query.json` +- AppPath detection using `which`, `command -v`, and common install locations +- Fixed crashes when `cFilesystem.get_user_folder()` wasn't implemented +- Better error messages when Sononym isn't installed or configured +- Fallback detection for non-standard installations + +### Bug Fixes +- Fixed crash in `parse_config()` when closing nil file handles +- Fixed Linux crashes when error messages were nil +- ConfigPath detection now sets full paths instead of just version names +- Better JSON config validation and error handling +- Detect button behavior improved for single vs multiple version scenarios + +## 0.52 + +- Add `changelog.md` +- `cLib.require()`, use for avoiding circular dependencies +- `cTable.is_indexed()`: check if table keys are exclusively numerical +- Add `cPersistence`, a replacement for `cDocument` (now deprecated) +- `cReflection`: several fixes/changes: + - `get_object_info()`: support objects without properties + - `get_object_info()`: return table instead of string + - `get_object_properties()`: hide implementation details + - `is_standard_type()`: accept any value (previously passed the 'type') + - `is_serializable_type()`: new method + +## 0.5 + +- Standalone version \ No newline at end of file diff --git a/Tools/com.renoise.Sononymph.xrnx/main.lua b/Tools/com.renoise.Sononymph.xrnx/main.lua index e992e733..9854c94b 100644 --- a/Tools/com.renoise.Sononymph.xrnx/main.lua +++ b/Tools/com.renoise.Sononymph.xrnx/main.lua @@ -1,150 +1,189 @@ - - ---[[=============================================================================================== -com.renoise.Sononymph.xrnx (main.lua) -===============================================================================================]]-- --[[ - - This tool adds Sononym integration to Renoise - -]] - ---------------------------------------------------------------------------------------------------- --- global variables ---------------------------------------------------------------------------------------------------- +This tool was originally created by danoise, +and somewhat heavily modified by Esa Ruoho a.k.a. Lackluster. +]]-- rns = nil -- reference to renoise.song() _trace_filters = nil -- don't show traces in console -_trace_filters = {".*"} +--_trace_filters = {".*"} --_trace_filters = {"^xOscClient"} - ---------------------------------------------------------------------------------------------------- --- required files ---------------------------------------------------------------------------------------------------- +-- Only show essential Sononym-related traces, filter out noisy UI/library traces +_trace_filters = { + "sononym%.db", "%.flac", "%.wav", "%.aiff", -- Show sample file info + "parsed selection:", "Found filename:", "selectedLocationPath:", -- Show Sononym data + "Selected Sample Full path:", "Sample loaded:", "Failed to load sample:" -- Show load results +} _clibroot = 'source/cLib/classes/' -_vlibroot = 'source/vLib/classes/' -_xlibroot = 'source/xLib/classes/' require (_clibroot..'cLib') require (_clibroot..'cDebug') require (_clibroot..'cFileMonitor') ---require (_clibroot..'cDocument') ---require (_clibroot..'cFilesystem') ---require (_clibroot..'cObservable') ---require (_clibroot..'cReflection') ---require (_clibroot..'cParseXML') ---require (_clibroot..'cSandbox') ---require (_clibroot..'cColor') - -cLib.require (_vlibroot..'vLib') -cLib.require (_vlibroot..'vDialog') -cLib.require (_vlibroot..'vToggleButton') ---cLib.require (_vlibroot..'vDialogWizard') ---cLib.require (_vlibroot..'vPrompt') ---cLib.require (_vlibroot..'vTable') - -cLib.require (_xlibroot..'xSample') ---cLib.require (_xlibroot..'xLFO') ---cLib.require (_xlibroot..'xAudioDevice') - -require ('source/AppPrefs') -require ('source/AppUI') -require ('source/AppUIAbout') -require ('source/App') +--------------------------------------------------------------------------------------------------- +-- Sample utility functions (replacement for xLib xSample functions) +--------------------------------------------------------------------------------------------------- + +-- Get sample buffer if it exists and has sample data +-- @param sample (renoise.Sample) +-- @return renoise.SampleBuffer or nil +function get_sample_buffer(sample) + TRACE("get_sample_buffer(sample)",sample) + + if sample.sample_buffer + and sample.sample_buffer.has_sample_data + then + return sample.sample_buffer + end +end + +-- Get sample name, as it appears in the sample-list (untitled samples included) +-- @param sample (renoise.Sample) +-- @param sample_idx (number) +-- @return string +function get_display_name(sample,sample_idx) + TRACE("get_display_name(sample,sample_idx)",sample,sample_idx) + assert(type(sample)=="Sample") + assert(type(sample_idx)=="number") + return (sample.name == "") + and ("Sample %02X"):format(sample_idx-1) + or sample.name +end --------------------------------------------------------------------------------------------------- --- local variables & initialization +-- AppPrefs class (needs to be defined before requiring other modules that use it) --------------------------------------------------------------------------------------------------- +class 'AppPrefs'(renoise.Document.DocumentNode) +--------------------------------------------------------------------------------------------------- +-- constructor, initialize with default values +function AppPrefs:__init() + renoise.Document.DocumentNode.__init(self) + self:add_property("autostart", renoise.Document.ObservableBoolean(false)) + self:add_property("autotransfercreatenew", renoise.Document.ObservableBoolean(false)) + self:add_property("autotransfercreateslot", renoise.Document.ObservableBoolean(false)) + self:add_property("polling_interval", renoise.Document.ObservableNumber(1)) + self:add_property("path_to_exe", renoise.Document.ObservableString("")) + self:add_property("path_to_config", renoise.Document.ObservableString("")) + self:add_property("show_transfer_warning", renoise.Document.ObservableBoolean(true)) + self:add_property("show_search_warning", renoise.Document.ObservableBoolean(true)) + self:add_property("show_prefs", renoise.Document.ObservableBoolean(true)) +end +require ('AppUI') +require ('App') + +--------------------------------------------------------------------------------------------------- +-- local variables & initialization +--------------------------------------------------------------------------------------------------- local TOOL_NAME = "Sononymph" -local TOOL_VERSION = "1.0" -local MIDI_PREFIX = "Tools:"..TOOL_NAME..":" +local TOOL_VERSION = "0.91" local prefs = AppPrefs() renoise.tool().preferences = prefs --- force all dialogs to have this name -vDialog.DEFAULT_DIALOG_TITLE = TOOL_NAME - local app = nil --------------------------------------------------------------------------------------------------- --- start application - function start(do_show) rns = renoise.song() if not app then app = App{ prefs = prefs, - tool_name = TOOL_NAME, + tool_name = "Sononymph", tool_version = TOOL_VERSION, waiting_to_show_dialog = prefs.autostart.value } end - if do_show then - app.ui:show() + if do_show then app.ui:show() end +end + + +-- Search Selected Sample function +local function search_selected_sample() + start(true) -- Open dialog + if app and app.ui.dialog and app.ui.dialog.visible then + app:search_selected_sample() end end +renoise.tool():add_midi_mapping{name="Sononymph:Toggle Auto-Transfer [Trigger]",invoke=function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_keybinding{name="Global:Sononymph:Toggle Auto-Transfer [Trigger]", invoke=function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_keybinding{name="Global:Sononymph:Open Sononymph Dialog...", invoke=function() start(true) end} +renoise.tool():add_menu_entry{name = "Instrument Box:Sononymph:Search Selected Sample", invoke = search_selected_sample} +renoise.tool():add_menu_entry{name = "Sample Editor:Sononymph:Search Selected Sample", invoke = search_selected_sample} +renoise.tool():add_menu_entry{name = "Sample Navigator:Sononymph:Search Selected Sample", invoke = search_selected_sample} + +renoise.tool():add_midi_mapping{name="Sononymph:Load Selected Sample from Sononym (Prompt) [Trigger]",invoke=function(message) if message:is_trigger() then start(false) if app then app:load_selected_sample_from_sononym(true) end end end} +renoise.tool():add_midi_mapping{name="Sononymph:Load Selected Sample from Sononym (No Prompt) [Trigger]",invoke=function(message) if message:is_trigger() then start(false) if app then app:load_selected_sample_from_sononym(false) end end end} +renoise.tool():add_menu_entry{name = "Instrument Box:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name = "Instrument Box:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} +renoise.tool():add_menu_entry{name = "Sample Editor:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name = "Sample Editor:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} +renoise.tool():add_menu_entry{name = "Sample Navigator:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name = "Sample Navigator:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} +renoise.tool():add_menu_entry{name = "Main Menu:Tools:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name = "Main Menu:Tools:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} +renoise.tool():add_keybinding{name = "Global:Sononymph:Load Selected Sample from Sononym (Prompt) [Trigger]", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_keybinding{name = "Global:Sononymph:Load Selected Sample from Sononym (No Prompt) [Trigger]", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} + +-- Random sample menu entry (COMMENTED OUT - flip_a_coin function doesn't work properly) +--[[ +renoise.tool():add_menu_entry{ + name = "Main Menu:Tools:Sononymph:Random Sample", + invoke = function() + start(false) -- Initialize app but don't show dialog + if app then + -- Use the original flip_a_coin function for actual random selection + flip_a_coin(app.prefs.path_to_config.value) + end + end +} +--]] + +-- Load selected sample from Sononym menu entry + +-- Random sample keybinding (COMMENTED OUT - flip_a_coin function doesn't work properly) +--[[ +renoise.tool():add_keybinding{ + name = "Global:Sononymph:Random Sample [Trigger]", + invoke = function() + start(false) -- Initialize app but don't show dialog + if app then + -- Use the original flip_a_coin function for actual random selection + flip_a_coin(app.prefs.path_to_config.value) + end + end +} +--]] --------------------------------------------------------------------------------------------------- -- tool menu entries - function register_tool_menu() - local str_name = "Main Menu:Tools:"..TOOL_NAME - local str_name_active = "Main Menu:Tools:"..TOOL_NAME.." (active)" + local str_name = "Main Menu:Tools:Sononymph:Sononymph..." + local str_name_active = "Main Menu:Tools:Sononymph:Sononymph (active)..." + if renoise.tool():has_menu_entry(str_name) then renoise.tool():remove_menu_entry(str_name) elseif renoise.tool():has_menu_entry(str_name_active) then renoise.tool():remove_menu_entry(str_name_active) end renoise.tool():add_menu_entry{ - name = (app and app.active) and str_name_active or str_name, - invoke = function() - start(true) - end - } + name = (app and app.monitor_active) and str_name_active or str_name, + invoke = function() start(true) end} end register_tool_menu() - --------------------------------------------------------------------------------------------------- -- notifications --------------------------------------------------------------------------------------------------- - renoise.tool().app_new_document_observable:add_notifier(function() TRACE("main app_new_document_observable fired...") - start(prefs.autostart.value) -end) - ---------------------------------------------------------------------------------------------------- --- keyboard/midi mappings - -local key_mapping, midi_mapping = nil,nil - -midi_mapping = MIDI_PREFIX.."Toggle Link [Trigger]" -renoise.tool():add_midi_mapping{ - name = midi_mapping, - invoke = function() - if app then - app:toggle_link() - end - end -} -key_mapping = "Global:"..TOOL_NAME..":".."Toggle Link [Trigger]" -renoise.tool():add_keybinding{ - name = key_mapping, - invoke = function(repeated) - if not repeated then - if app then - app:toggle_link() - end - end - end -} + start(prefs.autostart.value) end) + _AUTO_RELOAD_DEBUG = true + + + diff --git a/Tools/com.renoise.Sononymph.xrnx/manifest.xml b/Tools/com.renoise.Sononymph.xrnx/manifest.xml index 4fa9cae8..f6a9aa14 100644 --- a/Tools/com.renoise.Sononymph.xrnx/manifest.xml +++ b/Tools/com.renoise.Sononymph.xrnx/manifest.xml @@ -1,11 +1,15 @@ - SononymPilot + Sononymph with Paketti Modifications com.renoise.Sononymph - 5 - 0.1 - danoise [bjorn.nesby@gmail.com] + 0.91 + 6 + danoise & esaruoho + false Integration - This tool adds Sononym integration to Renoise. Launch from the Tools menu - http://renoise.com/tools/xStream + This tool adds Sononym integration to Renoise. Launch from the Tools menu +minor fixes by Esa Ruoho + http://patreon.com/esaruoho + + From 1a168f8a7fe531efebdae3efeaa101d9917e01f6 Mon Sep 17 00:00:00 2001 From: esaruoho Date: Sun, 6 Jul 2025 22:35:28 +0300 Subject: [PATCH 06/18] v0.92 more menu entries + sample navigator load selected sample to selected slot --- Tools/com.renoise.Sononymph.xrnx/App.lua | 95 +++ Tools/com.renoise.Sononymph.xrnx/README.md | 180 +++-- Tools/com.renoise.Sononymph.xrnx/changelog.md | 13 + Tools/com.renoise.Sononymph.xrnx/main.lua | 57 +- Tools/com.renoise.Sononymph.xrnx/manifest.xml | 6 +- .../source/cLib/README.md | 19 + .../source/cLib/changelog.md | 18 + .../source/cLib/classes/cBitmap.lua | 243 ++++++ .../source/cLib/classes/cColor.lua | 179 +++++ .../source/cLib/classes/cConfig.lua | 52 ++ .../source/cLib/classes/cConvert.lua | 122 +++ .../source/cLib/classes/cDebug.lua | 163 ++++ .../source/cLib/classes/cDocument.lua | 186 +++++ .../source/cLib/classes/cFileMonitor.lua | 234 ++++++ .../source/cLib/classes/cFilesystem.lua | 659 ++++++++++++++++ .../source/cLib/classes/cLib.lua | 496 ++++++++++++ .../source/cLib/classes/cNumber.lua | 120 +++ .../source/cLib/classes/cObservable.lua | 429 +++++++++++ .../source/cLib/classes/cParseXML.lua | 97 +++ .../source/cLib/classes/cPersistence.lua | 296 ++++++++ .../source/cLib/classes/cPreferences.lua | 660 ++++++++++++++++ .../source/cLib/classes/cProcessSlicer.lua | 127 ++++ .../source/cLib/classes/cReflection.lua | 245 ++++++ .../source/cLib/classes/cSandbox.lua | 377 ++++++++++ .../source/cLib/classes/cScheduler.lua | 148 ++++ .../source/cLib/classes/cString.lua | 238 ++++++ .../source/cLib/classes/cTable.lua | 235 ++++++ .../source/cLib/classes/cValue.lua | 89 +++ .../source/cLib/classes/cWaveform.lua | 707 ++++++++++++++++++ .../cLib/classes/support/slaxdom/slaxdom.lua | 49 ++ .../cLib/classes/support/slaxdom/slaxml.lua | 257 +++++++ 31 files changed, 6716 insertions(+), 80 deletions(-) create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/README.md create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/changelog.md create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cBitmap.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cColor.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConfig.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConvert.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDebug.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDocument.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFileMonitor.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFilesystem.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cLib.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cNumber.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cObservable.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cParseXML.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPersistence.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPreferences.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cProcessSlicer.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cReflection.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cSandbox.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cScheduler.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cString.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cTable.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cValue.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cWaveform.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxdom.lua create mode 100644 Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxml.lua diff --git a/Tools/com.renoise.Sononymph.xrnx/App.lua b/Tools/com.renoise.Sononymph.xrnx/App.lua index 0f24e0a0..2419ba3d 100644 --- a/Tools/com.renoise.Sononymph.xrnx/App.lua +++ b/Tools/com.renoise.Sononymph.xrnx/App.lua @@ -1094,6 +1094,101 @@ function App:load_selected_sample_from_sononym(show_prompt) end end +--------------------------------------------------------------------------------------------------- +-- Load currently selected sample from Sononym directly to the selected sample slot +-- This function is specifically for Sample Navigator context +function App:load_selected_sample_to_selected_slot() + TRACE("App:load_selected_sample_to_selected_slot()") + + -- Check if there's a selected sample slot + if not rns.selected_sample then + renoise.app():show_message("No sample slot selected.\nPlease select a sample slot first.") + return false + end + + -- Get the current selection directly from Sononym's JSON + local current_selection = App.parse_config(self.prefs.path_to_config.value) + if not current_selection then + renoise.app():show_message("Failed to get Sononym selection.\nMake sure Sononym has a file selected.") + return false + end + + -- Get the sample instance + local sample = rns.selected_sample + local sample_name = string.match(current_selection.filename, "([^/]+)$") or current_selection.filename + + -- Construct the full path (reusing the logic from do_transfer) + local config_folder,_,__ = cFilesystem.get_path_parts(self.prefs.path_to_config.value) + local folder,_,__ = cFilesystem.get_path_parts(current_selection.locationPath) + + local fpath + if (folder == config_folder) then + -- internal sononym library means filename is relative to the library folder + -- Remove 'sononym.db' from locationPath to get the base folder + local library_base = string.gsub(current_selection.locationPath, "sononym%.db$", "") + fpath = cFilesystem.unixslashes(library_base .. current_selection.filename) + else + -- external path, filename should be combined with folder + fpath = cFilesystem.unixslashes(folder .. current_selection.filename) + end + + TRACE("Selected Sample Full path for sample slot: " .. fpath) + + -- If the constructed path doesn't exist, try alternative constructions + if not io.exists(fpath) then + TRACE("Primary path doesn't exist, trying alternatives...") + + -- Try treating the filename as an absolute path + local alt_path1 = cFilesystem.unixslashes(current_selection.filename) + if io.exists(alt_path1) then + TRACE("Found file using filename as absolute path:", alt_path1) + fpath = alt_path1 + else + -- Try combining with the directory containing sononym.db + local alt_path2 = cFilesystem.unixslashes(folder .. "/" .. current_selection.filename) + if io.exists(alt_path2) then + TRACE("Found file using folder + filename with slash:", alt_path2) + fpath = alt_path2 + end + end + end + + -- Check if the file exists before attempting to load + if not io.exists(fpath) then + TRACE("File not found at constructed path:", fpath) + renoise.app():show_message("Failed to load sample:\nFile does not exist:\n" .. fpath) + return false + end + + -- Load the sample directly into the selected sample slot + local success,err = pcall(function() + return self:load_sample_with_slice_preservation(sample, fpath) + end) + + if not success then + TRACE("Loading failed with error:", err) + renoise.app():show_message("Failed to load sample:\n"..tostring(err)) + return false + end + + if not err then + TRACE("Loading failed - load_sample_with_slice_preservation returned false") + renoise.app():show_message("Failed to load sample: unknown error") + return false + end + + -- Update sample name + local folder,filename,ext = cFilesystem.get_path_parts(current_selection.filename) + sample.name = filename + + -- Display message in status bar + local msg = "Loaded sample to selected slot: "..filename + renoise.app():show_status(msg) + TRACE(msg) + + return true +end + --------------------------------------------------------------------------------------------------- -- Static methods --------------------------------------------------------------------------------------------------- diff --git a/Tools/com.renoise.Sononymph.xrnx/README.md b/Tools/com.renoise.Sononymph.xrnx/README.md index fdb89f36..20b35809 100644 --- a/Tools/com.renoise.Sononymph.xrnx/README.md +++ b/Tools/com.renoise.Sononymph.xrnx/README.md @@ -1,67 +1,133 @@ -# Sononym(ph) +# Sononymph - Paketti Modifications v0.92 ![Splash Image](docs/splash-large.png) -This tool is an integration of the Sononym sample browser into Renoise. Use it to browse for samples using the features in Sononym while listening to the result in the context of your Renoise project. - -## Features at a glance - -* Launch similarity search from the selected sample (Renoise → Sononym) -* Transfer samples from Sononym → Renoise -* Replace samples in Renoise while browsing in Sononym (auto-transfer) +**Sononymph** is a powerful integration tool that connects the Sononym sample browser with Renoise. This version includes extensive modifications and improvements by **Esa Ruoho (Lackluster)** for enhanced workflow and usability. + +**Sononymph** uses the cLib class library for the majority of its features. + +## Features + +### Core Functionality +- **Bidirectional integration** between Renoise and Sononym +- **Similarity search** - Launch searches from a selected Renoise sample +- **Sample transfer** - Import a sample from Sononym with a single click +- **Auto-transfer mode** - Automatically replace a sample while browsing +- **Slice preservation** - Maintain slice markers and settings during transfer +- **Smart path detection** - Automatically finds Sononym installations + +### Paketti Modifications +- **Multiple menu contexts** - Access Sononymph from Instrument Box, Sample Editor, Sample Navigator, and Main Menu Menu Entries. +- **MIDI mappings** - Search samples, toggle auto-transfer, and load samples via MIDI +- **Keyboard shortcuts** - Search samples, toggle auto-transfer, load samples, and open dialog +- **Enhanced auto-transfer** - Create new instruments or sample slots instead of just overwriting +- **Direct Sononym launch** - Start Sononym and browse folders from Renoise +- **Multi-version support** - Detect and select from multiple Sononym installations +- **Cross-platform support** - Improved Windows, macOS, and Linux compatibility + +## Quick Start + +1. **Launch Renoise** and start Sononymph from the Tools menu +2. **First-time setup** - Tool automatically detects your Sononym installation and query.json database location on Windows, macOS, or Linux +3. **Version detection** - If you install a new version of Sononym later, click "Detect" to find the new query.json folders and select from the dropdown menu +4. **Launch Sononym** (tool can do this for you via "Launch" button) +5. **Select a sample** in Renoise and click "Search in Sononym" +6. **Browse in Sononym** until you find something good +7. **Transfer** - Click "Transfer from Sononym" or enable auto-transfer mode + +## Interface Overview + +### Main Dialog +- **Renoise section** - Shows currently selected sample with detach button +- **Sononym section** - Displays selected sample and library info +- **Transfer controls** - Manual transfer button and auto-transfer toggle +- **Transfer options** - Create new instruments or sample slots +- **Options section** - Collapsible preferences panel + +### Transfer Options +- **Default** - Replace current sample (preserves slices) +- **Create New Instrument** - Create fresh instrument for every auto-transfer or user-triggered transfer +- **Create New Sample Slot** - Add new sample slot to current instrument for every auto-transfer or user-triggered transfer + +### Options Panel +- **AppPath** - Location of Sononym executable (auto-detected) +- **ConfigPath** - Location of query.json file (auto-detected) +- **Version dropdown** - Select from multiple detected Sononym versions +- **Status indicator** - Shows monitoring status and path validation +- **Direct links** - Sononym documentation and forum thread +- **Autostart** - Launch tool automatically with Renoise + +## Controls & Shortcuts + +### MIDI Mappings +- **`Sononymph:Search Selected Sample in Sononym [Trigger]`** - Select a sample in Renoise, press a MIDI mapping and Sononym searches for similar samples - a very fast workflow. +- **`Sononymph:Toggle Auto-Transfer [Trigger]`** - Turn auto-transfer on/off from your MIDI controller. Useful when you want to browse Sononym without accidentally loading samples into your current track. +- **`Sononymph:Load Selected Sample from Sononym (Prompt) [Trigger]`** - Have Sononym open, select a sample, press a MIDI mapping and Renoise loads the sample with a confirmation dialog. +- **`Sononymph:Load Selected Sample from Sononym (No Prompt) [Trigger]`** - Have Sononym open, select a sample, press a MIDI mapping and Renoise instantly loads the sample without a prompt - a very fast workflow. + +### Keybindings +- **`Global:Sononymph:Search Selected Sample in Sononym [Trigger]`** - Select a sample in Renoise, press a keyboard shortcut and Sononym searches for similar samples - a very fast workflow. +- **`Global:Sononymph:Toggle Auto-Transfer [Trigger]`** - Quickly enable/disable auto-transfer mode while working. Toggle off when browsing, on when you want samples to load automatically. +- **`Global:Sononymph:Open Sononymph Dialog...`** - Open the main Sononymph window without using menus. Fastest way to access the tool. +- **`Global:Sononymph:Load Selected Sample from Sononym (Prompt)`** - Have Sononym open, select a sample, press a keyboard shortcut and Renoise loads the sample with confirmation - a very fast workflow. +- **`Global:Sononymph:Load Selected Sample from Sononym (No Prompt)`** - Have Sononym open, select a sample, press a keyboard shortcut and Renoise instantly loads the sample - a very fast workflow. + +### Menu Entries +Right-click context menus provide quick access: +- **Instrument Box** - Right-click any instrument to search for similar samples in Sononym or load from Sononym. Perfect when organizing your instrument collection. +- **Sample Editor** - Right-click while editing samples to search for similar ones in Sononym or replace with Sononym samples. Great for sound design workflow. +- **Sample Navigator** - Right-click samples in the browser to search in Sononym, replace them, or load Sononym samples directly to the selected slot. Includes the new "Load Selected Sample to Selected Slot" function for targeted sample loading. Useful when going through sample folders. +- **Main Menu → Tools → Sononymph** - Access all functions from the traditional menu. Best for first-time setup or when you need the full dialog. + +## Platform Support + +### Windows +- Auto-detects installations in Program Files +- Supports standard and custom installation paths + +### macOS +- Finds apps in /Applications/Sononym.app +- Handles bundle structure automatically + +### Linux +- Searches PATH for 'sononym' executable +- Checks common installation locations +- Supports custom builds and package manager installations +- Fallback detection for non-standard setups + +## Auto-Transfer Mode + +When enabled, automatically imports samples as you browse in Sononym: +- **Preserves slice markers** and their settings +- **Switches to Sample Editor** view automatically +- **Configurable behavior** - overwrite, new instrument, or new slot +- **Real-time monitoring** of Sononym's selection changes + +## Troubleshooting + +### Path Detection +- **First-time setup** - Tool automatically detects Sononym installation and query.json database location on Windows, macOS, and Linux +- **New version detection** - When you install a new Sononym version, click "Detect" to find new query.json folders +- **Multiple versions** - Select from dropdown menu when multiple Sononym versions are found +- **Manual override** - "Browse" buttons allow manual path selection if auto-detection fails + +### Performance +- Tool monitors query.json file changes efficiently +- Lightweight native UI (no external dependencies) +- Minimal CPU usage when monitoring ## Links -* Youtube demonstration: TODO -* Download from tool page: http://renoise.com/tools/sononymph -* Discuss in Renoise forum: https://forum.renoise.com/t/new-tool-3-1-sononym-integration-preview/49660 -* Check for / report issues: https://github.com/renoise/xrnx/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sononymph - -## Quickstart - -1. Launch Renoise and start the tool from the Renoise tools menu. -Note that the tool needs to be configured when you launch it for the first time. -Follow the instructions on screen - see also the [preferences](#preferences) section below. -2. Launch Sononym - **IMPORTANT** ([more info](#search-in-sononym)). -3. Renoise: Select a sample and hit 'Search in Sononym' -4. Sononym: Browse around until you find something good -5. Renoise: Hit 'Transfer' to import the selected sample in Sononym - - -## The user interface - -![Screenshot](docs/screenshot.png) - -_#_ |Description -----|---------------- -1 | Displays the currently selected sample in Renoise.
Click the button to detach the instrument editor. -2 | Displays the currently selected sample in Sononym -3 | Displays the Sononym library that the sample belongs to -4 | [Launch a similarity search](#search-in-sononym) on the selected sample (1) -5 | Transfer the selected sample (2) from Sononym to Renoise -6 | Enable or disable the [automatic transfer mode](#auto-transfer) -7 | Decides if the tool starts automatically when Renoise is launched -8 | Open a dialog containing user instructions -9 | Specifies paths to [Sononym executable + configuration](#preferences) -10 | Click to auto-detect the Sononym paths (9) -11 | Click to open a file system dialog to set paths (9) -12 | Current tool status: "Monitoring...", "Invalid path" - -## Additional notes - -### Search in Sononym -Click this button to launch a similarity search in Sononym using the currently selected sample in Renoise as the source. - -**IMPORTANT: Sononym should be running _before_ launching a search** - -otherwise the Sononym process might lock Renoise. If you do this by -accident, simply close the Sononym window and start Sononym -from its usual place (Start Menu, Dock, etc). +- **Sononym Documentation**: https://www.sononym.net/docs/ +- **Forum Discussion**: https://forum.renoise.com/t/new-tool-3-4-sononymph-with-paketti-improvements-renoise-sononym-integration/76581 +- **Report Issues**: Create issues in your repository -### Auto-transfer -Enable this to automatically replace samples in Renoise while browsing in Sononym. The mode detects when the selection in Sononym has changed, and will automatically perform a 'transfer'. +## Credits -### Preferences +- **Original Tool**: danoise +- **Paketti Modifications**: Esa Ruoho (Lackluster): http://patreon.com/esaruoho +- **Version**: 0.92 - Added Sample Navigator selected slot loading functionality -#### Path to exe / path to config -The location of these paths depend on the operating system. The Sononym documentation specifies the typical locations, and how you can find them yourself: https://www.sononym.net/docs/installation/overview/) +--- +*This tool requires Sononym to be installed and configured. Visit [sononym.net](https://www.sononym.net/) to download Sononym.* diff --git a/Tools/com.renoise.Sononymph.xrnx/changelog.md b/Tools/com.renoise.Sononymph.xrnx/changelog.md index 9c24219c..66ec7ea5 100644 --- a/Tools/com.renoise.Sononymph.xrnx/changelog.md +++ b/Tools/com.renoise.Sononymph.xrnx/changelog.md @@ -1,5 +1,18 @@ # Changelog +## 0.92 + +### New Features +- **Sample Navigator Enhancement**: Added "Load Selected Sample to Selected Slot" function specifically for Sample Navigator context +- **Targeted Sample Loading**: Load samples from Sononym directly into the currently selected sample slot without creating new instruments +- **Smart Slot Loading**: Preserves slice markers and settings when loading into existing sample slots +- **Menu Entry**: New Sample Navigator menu entry for direct slot loading functionality + +### Technical Improvements +- Enhanced sample loading with `load_selected_sample_to_selected_slot()` function +- Improved error handling for sample slot selection validation +- Better user feedback with specific status messages for slot loading operations + ## 0.91 ### Major Changes diff --git a/Tools/com.renoise.Sononymph.xrnx/main.lua b/Tools/com.renoise.Sononymph.xrnx/main.lua index 9854c94b..2ba32f86 100644 --- a/Tools/com.renoise.Sononymph.xrnx/main.lua +++ b/Tools/com.renoise.Sononymph.xrnx/main.lua @@ -76,7 +76,7 @@ require ('App') -- local variables & initialization --------------------------------------------------------------------------------------------------- local TOOL_NAME = "Sononymph" -local TOOL_VERSION = "0.91" +local TOOL_VERSION = "0.92" local prefs = AppPrefs() renoise.tool().preferences = prefs @@ -106,27 +106,44 @@ local function search_selected_sample() end end -renoise.tool():add_midi_mapping{name="Sononymph:Toggle Auto-Transfer [Trigger]",invoke=function() start(false) if app then app:toggle_live_transfer() end end} -renoise.tool():add_keybinding{name="Global:Sononymph:Toggle Auto-Transfer [Trigger]", invoke=function() start(false) if app then app:toggle_live_transfer() end end} -renoise.tool():add_keybinding{name="Global:Sononymph:Open Sononymph Dialog...", invoke=function() start(true) end} -renoise.tool():add_menu_entry{name = "Instrument Box:Sononymph:Search Selected Sample", invoke = search_selected_sample} -renoise.tool():add_menu_entry{name = "Sample Editor:Sononymph:Search Selected Sample", invoke = search_selected_sample} -renoise.tool():add_menu_entry{name = "Sample Navigator:Sononymph:Search Selected Sample", invoke = search_selected_sample} - +renoise.tool():add_midi_mapping{name="Sononymph:Toggle Sononym Auto-Transfer [Trigger]",invoke=function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_midi_mapping{name="Sononymph:Open Sononymph Dialog...", invoke=function() start(true) end} +renoise.tool():add_midi_mapping{name="Sononymph:Search Selected Sample in Sononym [Trigger]",invoke=function() search_selected_sample() end} renoise.tool():add_midi_mapping{name="Sononymph:Load Selected Sample from Sononym (Prompt) [Trigger]",invoke=function(message) if message:is_trigger() then start(false) if app then app:load_selected_sample_from_sononym(true) end end end} renoise.tool():add_midi_mapping{name="Sononymph:Load Selected Sample from Sononym (No Prompt) [Trigger]",invoke=function(message) if message:is_trigger() then start(false) if app then app:load_selected_sample_from_sononym(false) end end end} -renoise.tool():add_menu_entry{name = "Instrument Box:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} -renoise.tool():add_menu_entry{name = "Instrument Box:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} -renoise.tool():add_menu_entry{name = "Sample Editor:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} -renoise.tool():add_menu_entry{name = "Sample Editor:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} -renoise.tool():add_menu_entry{name = "Sample Navigator:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} -renoise.tool():add_menu_entry{name = "Sample Navigator:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} -renoise.tool():add_menu_entry{name = "Main Menu:Tools:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} -renoise.tool():add_menu_entry{name = "Main Menu:Tools:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} -renoise.tool():add_keybinding{name = "Global:Sononymph:Load Selected Sample from Sononym (Prompt) [Trigger]", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} -renoise.tool():add_keybinding{name = "Global:Sononymph:Load Selected Sample from Sononym (No Prompt) [Trigger]", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} - --- Random sample menu entry (COMMENTED OUT - flip_a_coin function doesn't work properly) + +renoise.tool():add_keybinding{name="Global:Sononymph:Toggle Sononym Auto-Transfer [Trigger]", invoke=function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_keybinding{name="Global:Sononymph:Open Sononymph Dialog...", invoke=function() start(true) end} +renoise.tool():add_keybinding{name="Global:Sononymph:Search Selected Sample in Sononym", invoke=search_selected_sample} +renoise.tool():add_keybinding{name="Global:Sononymph:Load Selected Sample from Sononym (Prompt) [Trigger]", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_keybinding{name="Global:Sononymph:Load Selected Sample from Sononym (No Prompt) [Trigger]", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} + +renoise.tool():add_menu_entry{name="Instrument Box:Sononymph:Open Sononymph Dialog...", invoke = function() start(true) end} +renoise.tool():add_menu_entry{name="--Instrument Box:Sononymph:Toggle Sononym Auto-Transfer", invoke = function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_menu_entry{name="Instrument Box:Sononymph:Search Selected Sample in Sononym", invoke = search_selected_sample} +renoise.tool():add_menu_entry{name="Instrument Box:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name="Instrument Box:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} + +renoise.tool():add_menu_entry{name="Sample Editor:Sononymph:Open Sononymph Dialog...", invoke = function() start(true) end} +renoise.tool():add_menu_entry{name="--Sample Editor:Sononymph:Toggle Sononym Auto-Transfer", invoke = function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_menu_entry{name="Sample Editor:Sononymph:Search Selected Sample in Sononym", invoke = search_selected_sample} +renoise.tool():add_menu_entry{name="Sample Editor:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name="Sample Editor:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} + +renoise.tool():add_menu_entry{name="Sample Navigator:Sononymph:Open Sononymph Dialog...", invoke = function() start(true) end} +renoise.tool():add_menu_entry{name="--Sample Navigator:Sononymph:Toggle Sononym Auto-Transfer", invoke = function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_menu_entry{name="Sample Navigator:Sononymph:Search Selected Sample in Sononym", invoke = search_selected_sample} +renoise.tool():add_menu_entry{name="Sample Navigator:Sononymph:Load Selected Sample to Selected Slot", invoke = function() start(false) if app then app:load_selected_sample_to_selected_slot() end end} +renoise.tool():add_menu_entry{name="Sample Navigator:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name="Sample Navigator:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} + +renoise.tool():add_menu_entry{name="Main Menu:Tools:Sononymph:Open Sononymph Dialog...", invoke = function() start(true) end} +renoise.tool():add_menu_entry{name="--Main Menu:Tools:Sononymph:Toggle Sononym Auto-Transfer", invoke = function() start(false) if app then app:toggle_live_transfer() end end} +renoise.tool():add_menu_entry{name="Main Menu:Tools:Sononymph:Search Selected Sample in Sononym", invoke = search_selected_sample} +renoise.tool():add_menu_entry{name="Main Menu:Tools:Sononymph:Load Selected Sample from Sononym (Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(true) end end} +renoise.tool():add_menu_entry{name="Main Menu:Tools:Sononymph:Load Selected Sample from Sononym (No Prompt)", invoke = function() start(false) if app then app:load_selected_sample_from_sononym(false) end end} + +-- Random sample menu entry (COMMENTED OUT - flip_a_coin function not yet supported by Sononym) --[[ renoise.tool():add_menu_entry{ name = "Main Menu:Tools:Sononymph:Random Sample", diff --git a/Tools/com.renoise.Sononymph.xrnx/manifest.xml b/Tools/com.renoise.Sononymph.xrnx/manifest.xml index f6a9aa14..7cc95208 100644 --- a/Tools/com.renoise.Sononymph.xrnx/manifest.xml +++ b/Tools/com.renoise.Sononymph.xrnx/manifest.xml @@ -2,13 +2,13 @@ Sononymph with Paketti Modifications com.renoise.Sononymph - 0.91 + 0.92 6 danoise & esaruoho false Integration - This tool adds Sononym integration to Renoise. Launch from the Tools menu -minor fixes by Esa Ruoho + This tool adds Sononym integration to Renoise. Launch from the Tools/Instrument Box/Sample Editor/Sample Navigator etc menus, midimapping or keybinding. +major fixes by Esa Ruoho http://patreon.com/esaruoho diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/README.md b/Tools/com.renoise.Sononymph.xrnx/source/cLib/README.md new file mode 100644 index 00000000..c1c61f47 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/README.md @@ -0,0 +1,19 @@ +# About cLib + +cLib is pure lua library which can make scripting with the Renoise API a bit easier. +The library contains methods for working with the file system, basic data-types (string, table and so on), as well other lua/Renoise API-specific details. + +## Documentation + +Point your browser to this location to browse the auto-generated luadocs: +https://renoise.github.io/luadocs/clib + +## Debugging with cLib + +As an alternative to using print statements in your code, you can call the TRACE/LOG methods. + +**LOG** = Print to console +**TRACE** = Print debug info (when debugging is enabled) + +cLib comes with a dedicated class for debugging called cDebug. Including this class will replace the standard TRACE and LOG methods with more sophisticated versions. + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/changelog.md b/Tools/com.renoise.Sononymph.xrnx/source/cLib/changelog.md new file mode 100644 index 00000000..5ef05de8 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/changelog.md @@ -0,0 +1,18 @@ +# Changelog + +## 0.52 + +- Add `changelog.md` +- `cLib.require()`, use for avoiding circular dependencies +- `cTable.is_indexed()`: check if table keys are exclusively numerical +- Add `cPersistence`, a replacement for `cDocument` (now deprecated) +- `cReflection`: several fixes/changes: + - `get_object_info()`: support objects without properties + - `get_object_info()`: return table instead of string + - `get_object_properties()`: hide implementation details + - `is_standard_type()`: accept any value (previously passed the 'type') + - `is_serializable_type()`: new method + +## 0.5 + +- Standalone version \ No newline at end of file diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cBitmap.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cBitmap.lua new file mode 100644 index 00000000..0a8092b5 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cBitmap.lua @@ -0,0 +1,243 @@ +--[[============================================================================ +cBitmap.lua +============================================================================]]-- + +class 'cBitmap' + +-------------------------------------------------------------------------------- +--- Basebones class for for creating .bmp files +-- Each line is padded with zeroes, like this +-- +-- | | | | | | | | | | | +-- ff66ccff66ccff66cc000000| | | | | +-- ff66ccff66ccff66ccff66cc| | | | | +-- ff66ccff66ccff66ccff66ccff66cc00| | | +-- ff66ccff66ccff66ccff66ccff66ccff66cc0000| + +cBitmap.BIT_COUNT = {1,4,8,16,24,32} + +--[[ +cBitmap.BMP_COMPRESSION = { + UNCOMPRESSED = 0, + RLE_8 = 1, -- Usable only with 8-bit images + RLE_4 = 2, -- Usable only with 4-bit images + BITFIELDS = 3, -- Used - and required - only with 16- and 32-bit images +} +]] + +cBitmap.HEADER_SIZE = 54 +cBitmap.PIXELS_PER_METER = 2834 + +function cBitmap:__init(...) + + local args = cLib.unpack_args(...) + + self.width = args.width or 100 + + self.height = args.height or 100 + + -- (cBitmap.BIT_COUNT) NB: only 24-bit is tested!! + self.bit_count = args.bit_count or 24 + + --self.compression = 0 -- uncompressed + --self.size_image = 0 -- no need when uncompressed + --self.x_resolution = 0 -- preferred horizontal resolution + --self.y_resolution = 0 -- preferred vertical resolution + --self.clrs_used = 0 -- used Number Color Map entries + --self.clrs_important = 0 -- Number of significant colors + + self.pixels = {} + + -- internal ------------------------- + + self.bitmap = {} + + + +end + +------------------------------------------------------------------------------- +--- set all pixels to a particular color + +function cBitmap:flood(color) + + self.pixels = {} + + for k = 1,self.height*self.width do + self.pixels[k] = color + end + +end + +------------------------------------------------------------------------------- +--- given a value, return bytes in reverse (endian) order +-- @param num (number), treated as 32-bit +-- @param endian (bool), whether to swap bytes or not +-- @return table>number + +function cBitmap.split_bits(num,endian) + + local str_hex = (bit.tohex(num)) + local rslt = {} + local bytes = { + tonumber(string.sub(str_hex,1,2),16), + tonumber(string.sub(str_hex,3,4),16), + tonumber(string.sub(str_hex,5,6),16), + tonumber(string.sub(str_hex,7,8),16), + } + if endian then + for k,v in ripairs(bytes) do + table.insert(rslt,v) + end + else + rslt = bytes + end + + return rslt + +end + +------------------------------------------------------------------------------- + +function cBitmap:get_bytes_padding() + --TRACE("cBitmap:get_bytes_padding()",self) + return (self.width)%4 +end + +------------------------------------------------------------------------------- + +function cBitmap:get_bitmap_size_in_bytes() + --TRACE("cBitmap:get_bitmap_size_in_bytes()",self) + local padding = self:get_bytes_padding() + return (self.width*self.height*3) + padding*self.height+2 +end + +------------------------------------------------------------------------------- + +function cBitmap:get_total_size_in_bytes() + --TRACE("cBitmap:get_total_size_in_bytes()",self) + return cBitmap.HEADER_SIZE + self:get_bitmap_size_in_bytes() +end + +------------------------------------------------------------------------------- +--- this method will create the bitmap table + +function cBitmap:create() + + self.bitmap = {} + + local size_bytes = cBitmap.split_bits(self:get_total_size_in_bytes(),true) + local bitmap_bytes = cBitmap.split_bits(self:get_bitmap_size_in_bytes(),true) + local width_bytes = cBitmap.split_bits(self.width,true) + local height_bytes = cBitmap.split_bits(self.height,true) + local ppm_bytes = cBitmap.split_bits(cBitmap.PIXELS_PER_METER,true) + + -- FILE HEADER ---------------------- + + -- bitmap signature + self.bitmap[1] = 'B' + self.bitmap[2] = 'M' + + -- file size in bytes + for i = 3, 6 do self.bitmap[i] = size_bytes[i-2] end + for i = 7, 10 do self.bitmap[i] = 0 end -- reserved fields + + -- offset of pixel data (after header) + self.bitmap[11] = cBitmap.HEADER_SIZE + self.bitmap[12] = 0 + self.bitmap[13] = 0 + self.bitmap[14] = 0 + + -- BITMAP HEADER -------------------- + + -- header size + self.bitmap[15] = 40 + for i = 16, 18 do self.bitmap[i] = 0 end + for i = 19, 22 do self.bitmap[i] = width_bytes[i-18] end + for i = 23, 26 do self.bitmap[i] = height_bytes[i-22] end + + self.bitmap[27] = 1 -- reserved field + self.bitmap[28] = 0 + self.bitmap[29] = self.bit_count -- number of bits per pixel + self.bitmap[30] = 0 + + for i = 31, 34 do self.bitmap[i] = 0 end -- compression method + for i = 35, 38 do self.bitmap[i] = bitmap_bytes[i-34] end + for i = 39, 42 do self.bitmap[i] = ppm_bytes[i-38] end + for i = 43, 46 do self.bitmap[i] = ppm_bytes[i-42] end + for i = 47, 50 do self.bitmap[i] = 0 end -- color palette + for i = 51, 54 do self.bitmap[i] = 0 end -- # important colors + + -- PIXEL DATA ----------------------- + + local num_pixel_bytes = self:get_bitmap_size_in_bytes() + local padding = self:get_bytes_padding() + + --local row_idx = 1 + local col_idx = 1 + local pixel_idx = 1 + local write_pos = 55 + + local bytes_per_row = self.width*3 + padding + + for i = 55, 55+num_pixel_bytes do + + if (i < write_pos) then + -- wait for counter to catch up + else + + local bytes_written = 0 + local pixel + + -- write a line of pixels (3 bytes), pad if needed + for col_idx = 0, self.width-1 do + pixel = self.pixels[pixel_idx] + if pixel then + local write_pos = i+(col_idx*3) + self.bitmap[write_pos+0] = pixel[3] + self.bitmap[write_pos+1] = pixel[2] + self.bitmap[write_pos+2] = pixel[1] + bytes_written = bytes_written + 3 + pixel_idx = pixel_idx + 1 + end + end + if pixel then + if (bytes_written < bytes_per_row) then + for pad_idx = 0,padding-1 do + local write_pos = i+(self.width*3)+pad_idx + self.bitmap[write_pos] = 0 + end + bytes_written = bytes_written + padding + end + end + write_pos = #self.bitmap+1 + + end + + end + + +end + +------------------------------------------------------------------------------- + +function cBitmap:save_bmp(file_path) + TRACE("cBitmap.save_bmp(file_path)",file_path) + + local fh = io.open(file_path,'wb') + if not fh then + error("failed to create file handler") + end + + for k,v in ipairs(self.bitmap) do + if (type(v)=="string") then + fh:write(string.char(string.byte(v))) + else + fh:write(string.char(v)) + end + end + + fh:close() + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cColor.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cColor.lua new file mode 100644 index 00000000..bce37efe --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cColor.lua @@ -0,0 +1,179 @@ +--[[============================================================================ +cColor +============================================================================]]-- +--[[ + + Static methods for dealing with color + +]] + +class 'cColor' + +-------------------------------------------------------------------------------- +-- brighten/darken color by variable amount, using HSV color space +-- @param t (table{r,g,b}) +-- @param amt (number) 0 = black, 0.5 = neutral, 1 = white +-- @return table + +function cColor.adjust_brightness(rgb,amt) + TRACE("cColor.adjust_brightness(rgb,amt)",rgb,amt) + + rgb = table.rcopy(rgb) + + local hsv = cColor.rgb_to_hsv(rgb) + if (amt > 0.5) then + local factor = (amt-0.5)*2 + hsv[3] = hsv[3] + ((1-hsv[3]) * factor) + else + local factor = 1-(amt*2) + hsv[3] = hsv[3] * factor + end + + return cColor.hsv_to_rgb(hsv) + +end + +-------------------------------------------------------------------------------- +-- get average from color +-- @param color (table) +-- @return number + +function cColor.get_average(color) + return (color[1]+color[2]+color[3])/3 +end + +-------------------------------------------------------------------------------- +-- convert r,g,b into numeric representation valid for hex display (#RRGGBB) +-- @param t (table) +-- @return number + +function cColor.color_table_to_value(t) + return t[1]*0x10000 + t[2]*0x100 + t[3] +end + +-------------------------------------------------------------------------------- +-- convert r,g,b table to string representation (e.g. "0xFFCC66") +-- @param t (table) +-- @param [prefix], string - e.g. "#" to return CSS-style color +-- @return string + +function cColor.color_table_to_hex_string(t,prefix) + + local val = cColor.color_table_to_value(t) + return cColor.value_to_hex_string(val,prefix) + +end + +-------------------------------------------------------------------------------- +-- convert numeric representation into r,g,b table +-- @param val (int) +-- @return (table) + +function cColor.value_to_color_table(val) + local r = math.floor(val/0x10000) + local g = math.floor(val/0x100) - (r*0x100) + local b = val - ((r*0x10000)+(g*0x100)) + return {r,g,b} +end + +-------------------------------------------------------------------------------- +-- convert value to hexadecimal string (e.g. 0xFFCC66) +-- @param val (int) +-- @param [prefix], string - e.g. "#" to return CSS-style color +-- @return string + +function cColor.value_to_hex_string(val,prefix) + if not prefix then + prefix = "0x" + end + return ("%s%.6X"):format(prefix,val) +end + +-------------------------------------------------------------------------------- +-- convert hexadecimal string to value +-- @param str_val (string), e.g. "5FEC99", "#5FEC99" or "0x5FEC99" +-- @return int or nil if unable to convert + +function cColor.hex_string_to_value(str_val) + + -- strip prefixes "#" or "0x" + if (string.sub(str_val,1,1)=="#") then + str_val = string.sub(str_val,2,#str_val) + end + + if (string.sub(str_val,1,2)=="0x") then + str_val = string.sub(str_val,3,#str_val) + end + + local rslt = tonumber("0x"..str_val) + + if rslt and (rslt <= 0xFFFFFF) then + return rslt + end + +end + +-------------------------------------------------------------------------------- +-- Converts an RGB color value to HSV. Conversion formula +-- adapted from http://en.wikipedia.org/wiki/HSV_color_space. +-- @param rgb (table), the RGB representation +-- @return table, the HSV representation + +function cColor.rgb_to_hsv(rgb) + local r, g, b = rgb[1] / 255, rgb[2] / 255, rgb[3] / 255 + local max, min = math.max(r, g, b), math.min(r, g, b) + local h, s, v + v = max + + local d = max - min + if max == 0 then s = 0 else s = d / max end + + if max == min then + h = 0 -- achromatic + else + if max == r then + h = (g - b) / d + if g < b then h = h + 6 end + elseif max == g then h = (b - r) / d + 2 + elseif max == b then h = (r - g) / d + 4 + end + h = h / 6 + end + + return {h,s,v} +end + +-------------------------------------------------------------------------------- +-- Converts an HSV color value to RGB. Conversion formula +-- adapted from http://en.wikipedia.org/wiki/HSV_color_space. +-- @param hsv (table), the HSV representation +-- @return table, the RGB representation + +function cColor.hsv_to_rgb(hsv) + + local h, s, v = hsv[1],hsv[2],hsv[3] + local r, g, b + + local i = math.floor(h * 6); + local f = h * 6 - i; + local p = v * (1 - s); + local q = v * (1 - f * s); + local t = v * (1 - (1 - f) * s); + + i = i % 6 + + if i == 0 then r, g, b = v, t, p + elseif i == 1 then r, g, b = q, v, p + elseif i == 2 then r, g, b = p, v, t + elseif i == 3 then r, g, b = p, q, v + elseif i == 4 then r, g, b = t, p, v + elseif i == 5 then r, g, b = v, p, q + end + + return { + math.floor(r * 255), + math.floor(g * 255), + math.floor(b * 255) + } + +end diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConfig.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConfig.lua new file mode 100644 index 00000000..88f79db2 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConfig.lua @@ -0,0 +1,52 @@ +--[[============================================================================ +-- cConfig +============================================================================]]-- + +--[[-- + +Static methods for accessing the Renoise config file +. +# + +]] + +--============================================================================== + +require (_clibroot.."cFilesystem") +require (_clibroot.."cParseXML") + +class 'cConfig' + +-- table, Config.xml (parsed) +cConfig.xml = nil + + +------------------------------------------------------------------------------- + +function cConfig.load_config() + + local config_fpath = cFilesystem.get_userdata_folder().."Config.xml" + cConfig.xml = cParseXML.load_and_parse(config_fpath) + +end + +------------------------------------------------------------------------------- +-- retrieve a property value from the config file +-- @return string or nil + +function cConfig.get_value(xpath) + TRACE("cConfig.get_value(xpath)",xpath) + + if not cConfig.xml then + cConfig.load_config() + end + + local node = cParseXML.get_node_by_path(cConfig.xml,xpath) + if node then + local val = cParseXML.get_node_value(node) + return val + end + +end + + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConvert.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConvert.lua new file mode 100644 index 00000000..ed63f7e8 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cConvert.lua @@ -0,0 +1,122 @@ +--[[=============================================================================================== +cConvert +===============================================================================================]]-- + +--[[-- + +Various static conversion methods +. + +NB: Renoise finetuning is expressed as a value between -127 and 127. + +]] + +--================================================================================================= + +class 'cConvert' + +--------------------------------------------------------------------------------------------------- +-- [Static] Convert note to hertz +-- @param note (number) +-- @param hz_ini (number) [optional] +-- @return number + +function cConvert.note_to_hz(note,hz_ini) + TRACE('cConvert.note_to_hz(note,hz_ini)',note,hz_ini) + hz_ini = hz_ini or 440 + return math.pow(2, (note-57)/12) * hz_ini; +end + +--------------------------------------------------------------------------------------------------- +-- [Static] convert hertz to note value +-- @param freq (number) +-- @param hz_ini (number) [optional] +-- @return note (number), cents (number between 0 and 1) + +function cConvert.hz_to_note(freq,hz_ini) + TRACE('cConvert.hz_to_note(freq,hz_ini)',freq,hz_ini) + hz_ini = hz_ini or 440 + local lnote = (math.log(freq)-math.log(hz_ini))/math.log(2)+4; + local oct = math.floor(lnote); + local cents = 1200*(lnote-oct); + local note_num = math.floor(cents/100)%12; + cents = cents - note_num*100; + if (cents > 50) then + cents = cents-100; + note_num = note_num+1 + end + + return (note_num + 9) + (oct*12), cents + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Note to frames - e.g. 48 (C-4) -> 169 +-- @param note (number) +-- @param sample_rate (number) +-- @param hz_ini (number) +-- @return number (floor), number (with fractional part) + +function cConvert.note_to_frames(note,sample_rate,hz_ini) + TRACE("cConvert.note_to_frames(note,sample_rate,hz_ini)",note,sample_rate,hz_ini) + hz_ini = hz_ini or 440 + local frame = ((1/2)^((note-57)/12)) * (sample_rate/hz_ini) + return math.floor(frame),frame +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Note to frames - e.g. 48 (C-4) -> 169 +-- @param note (number) +-- @param sample_rate (number) +-- @param hz_ini (number) +-- @return number (rounded), number (with fractional part) + +function cConvert.frames_to_note(frames,sample_rate,hz_ini) + TRACE("cConvert.frames_to_note(frames,sample_rate,hz_ini)",frames,sample_rate,hz_ini) + local hz = cConvert.frames_to_hz(frames,sample_rate,hz_ini) + --print("*** hz",hz) + return cConvert.hz_to_note(hz,hz_ini) + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Frames to hertz +-- @param frames (frames) +-- @param sample_rate (number) +-- @param transpose (number), optional transpose amount (defaults to C-4/48) +-- @return number, value in hertz + +function cConvert.frames_to_hz(frames,sample_rate,transpose) + TRACE("cConvert.frames_to_hz(frames,sample_rate,transpose)",frames,sample_rate,transpose) + local frames_srate = sample_rate/frames + if transpose then + local base_hz = cConvert.note_to_hz(48) + local transp_hz = cConvert.note_to_hz(transpose) + return (transp_hz / base_hz) * frames_srate + else + return frames_srate + end + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Hertz to frames +-- @param hz (frames) +-- @param sample_rate (number) +-- @param transpose (number), optional transpose amount (defaults to C-4/48) +-- @return number (rounded), number (with fractional part) + +function cConvert.hz_to_frames(hz,sample_rate,transpose) + TRACE("cConvert.hz_to_frames(hz,sample_rate,transpose)",hz,sample_rate,transpose) + + local frames = sample_rate/hz + if transpose then + local base_hz = cConvert.note_to_hz(48) + local transp_hz = cConvert.note_to_hz(transpose) + frames = frames * (transp_hz / base_hz) + end + + return cLib.round_value(frames),frames + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDebug.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDebug.lua new file mode 100644 index 00000000..ecf883ce --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDebug.lua @@ -0,0 +1,163 @@ +--[[============================================================================ +cDebug +============================================================================]]-- + +--[[-- + +Debug tracing & logging +. +# + +Set one or more expressions to either show all or only a few messages +from `TRACE` calls. + +Some examples: + {".*"} -> show all traces + {"^Display:"} " -> show traces, starting with "Display:" only + {"^ControlMap:", "^Display:"} -> show "Display:" and "ControlMap:" + +]] + +--============================================================================== + +--_trace_filters = {".*"} + +require (_clibroot.."cFilesystem") + +class 'cDebug' + +-------------------------------------------------------------------------------- + +function cDebug.serialize(obj) + local succeeded, result = pcall(tostring, obj) + if succeeded then + return result + else + return "???" + end +end + +-------------------------------------------------------------------------------- + +function cDebug.concat_args(...) + + local result = "" + + -- concat args to a string + local n = select('#', ...) + for i = 1, n do + local obj = select(i, ...) + if( type(obj) == 'table') then + result = result .. cDebug.rdump(obj) + else + result = result .. cDebug.serialize(select(i, ...)) + if (i ~= n) then + result = result .. "\t" + end + end + end + + return result + +end + +-------------------------------------------------------------------------------- + +function cDebug.rdump(t, indent, done) + + local result = "\n" + done = done or {} + indent = indent or string.rep(' ', 2) + + local next_indent + for key, value in pairs(t) do + if (type(value) == 'table' and not done[value]) then + done[value] = true + next_indent = next_indent or (indent .. string.rep(' ', 2)) + result = result .. indent .. '[' .. cDebug.serialize(key) .. '] => table\n' + cDebug.rdump(value, next_indent .. string.rep(' ', 2), done) + else + result = result .. indent .. '[' .. cDebug.serialize(key) .. '] => ' .. + cDebug.serialize(value) .. '\n' + end + end + + return result +end + +-------------------------------------------------------------------------------- +-- calling this will iterate through the entire tool and remove all TRACE +-- statements (for internal use only!!) + +function cDebug.remove_trace_statements() + + local msg = "Remove all TRACE statements from source files?" + local choice = renoise.app():show_prompt("Confirm",msg,{"OK","Cancel"}) + if (choice == "Cancel") then + return + end + + local str_path = renoise.tool().bundle_path + local file_ext = {"*.lua"} + + -- @return false to stop recursion + local callback_fn = function(path,file,type) + + if (type == cFilesystem.FILETYPE.FILE) then + local file_path = path .. "/"..file + local str_text,err = cFilesystem.load_string(file_path) + if not str_text then + if err then + renoise.app():show_warning(err) + end + return false + end + local str_new = string.gsub(str_text,"\n%s*TRACE([^\n]*","") + local passed,err = cFilesystem.write_string_to_file(file_path,str_new) + if not passed then + if err then + renoise.app():show_warning(err) + end + return false + end + + end + + return true + + end + + cFilesystem.recurse(str_path,callback_fn,file_ext) + +end + +--============================================================================== +-- Global namespace +--============================================================================== + +--- TRACE implementation, provide detailed, filtered output +-- @param (vararg) + +if (_trace_filters ~= nil) then + + function TRACE(...) + + local result = cDebug.concat_args(...) + + -- apply filter + for _,filter in pairs(_trace_filters) do + if result:find(filter) then + print (result) + break + end + end + end + +else + + function TRACE() + -- do nothing + end + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDocument.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDocument.lua new file mode 100644 index 00000000..0dc43ea4 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cDocument.lua @@ -0,0 +1,186 @@ +--[[============================================================================ +-- cDocument +============================================================================]]-- + +--[[-- + +Create lightweight classes with import/export features +. +# + +For the cDocument to work, you need to define a static DOC_PROPS property. +You can define integers, floating point values, strings and booleans + + MyClass.DOC_PROPS = { + { + name = "my_integer", -- class/property name + title = "IntegerValue", -- display name + value_min = 1, -- only for numbers + value_max = 12, -- -//- + value_quantum = 1, -- -//- + value_default = 4, -- always define this!! + }, + { + name = "my_float", + title = "A floating point value between 0-1", + value_min = 0, + value_max = 1, + value_default = 0.0, + }, + } + + + +FIXME + * Re-implement as ipairs (changed implementation) + + +--]] + +--============================================================================== + +require (_clibroot.."cReflection") + +class 'cDocument' + + +-------------------------------------------------------------------------------- +-- import serialized values that match one of our DOC_PROPS +-- @param str (string) serialized values + +function cDocument:import(str) + + assert(type(str)=="string") + + local t = cDocument.deserialize(str,self.DOC_PROPS) + for k,v in pairs(t) do + self[k] = v + end + +end + +-------------------------------------------------------------------------------- +-- @return string, serialized values + +function cDocument:export() + + local t = cDocument.serialize(self,self.DOC_PROPS) + return cLib.serialize_table(t) + +end + +-------------------------------------------------------------------------------- +-- collect properties from object +-- @param obj (class instance) +-- @param props (table) DOC_PROPS +-- @return table + +function cDocument.serialize(obj,props) + + assert(type(props)=="table") + + local t = {} + for k,v in pairs(props) do + local property_type = props[k] + if property_type then + t[k] = cReflection.cast_value(obj[k],property_type) + else + t[k] = obj[k] + end + end + return t + +end + +-------------------------------------------------------------------------------- +-- deserialize string +-- @param str (str) serialized string +-- @param props (table) DOC_PROPS +-- @return table or nil + +function cDocument.deserialize(str,props) + + assert(type(str)=="string") + assert(type(props)=="table") + + local t = loadstring("return "..str) + local deserialized = t() + if not deserialized then + return + end + + t = {} + for k,v in pairs(props) do + if deserialized[k] then + local property_type = v + if property_type then + t[k] = cReflection.cast_value(deserialized[k],property_type) + else + t[k] = deserialized[k] + end + end + end + + return t + +end + +-------------------------------------------------------------------------------- +-- find property descriptor by key +-- @return table or nil + +function cDocument.get_property(props,key) + TRACE("cDocument.get_property(props,key)",props,key) + + assert(type(key)=="string") + assert(type(props)=="table") + + for k,v in ipairs(props) do + if (v.name == key) then + return v,k + end + end + +end + +-------------------------------------------------------------------------------- +-- apply value to object +-- * will clamp/cast value when outside range / wrong type +-- @return value (boolean,string,number) + +function cDocument.apply_value(obj,prop,val) + TRACE("cDocument.apply_value(obj,prop,val)",obj,prop,val) + + assert(type(prop)=="table") + + if type(prop.value_default)=="boolean" then + + elseif type(prop.value_default)=="string" then + + elseif type(prop.value_default)=="number" then + + if prop.value_max and prop.value_min then + if (val > prop.value_max) then + LOG("Clamp to range") + val = prop.value_max + elseif (val < prop.value_min) then + LOG("Clamp to range") + val = prop.value_min + end + end + + if prop.zero_based then + val = val+1 + end + + else + error("Unsupported value type") + end + + if not (obj[prop.name] == val) then + obj[prop.name] = val + return val + end + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFileMonitor.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFileMonitor.lua new file mode 100644 index 00000000..489612c0 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFileMonitor.lua @@ -0,0 +1,234 @@ + +--[[ + + Monitor files by checking their modification date - + + ## How to respond to changes: + + * The class emits "file_modified" events when change is detected + * Respond to this event by accessing "changed" + +]] + +--------------------------------------------------------------------------------------------------- + +class "cFileMonitor" + +--------------------------------------------------------------------------------------------------- +-- Constructor +-- @param polling_interval (number), seconds, 0 = as often as possible) +-- @param emit_initial (boolean), when true change is emitted while initially adding files + +function cFileMonitor:__init(...) + + local args = cLib.unpack_args(...) + + -- validate options + if args.polling_interval then + assert(type(args.polling_interval == "number")) + end + if args.emit_initial then + assert(type(args.emit_initial == "boolean")) + end + + -- properties + + -- table, currently watched files + self.paths = property(self.get_paths,self.set_paths) + self._paths = {} + + -- boolean, true when actively monitoring files + self.is_monitoring = property(self.get_is_monitoring) + self._is_monitoring = renoise.Document.ObservableBoolean(false) + + -- polling interval (seconds, 0 = as often as possible) + self.polling_interval = property(self.get_polling_interval,self.set_polling_interval) + self.polling_interval_observable = renoise.Document.ObservableNumber(args.polling_interval or 0) + + -- boolean - + self.emit_initial = true + + -- events + + -- table + -- read-only: most recently changed files + self.changed = property(self.get_changed) + + -- use this to get notified when files have changed + self.changed_observable = renoise.Document.ObservableBang() + + -- private + + -- number + self._last_poll = nil + + -- (table, key is filename and return value from io.stat() is value) + self._stats = {} + + -- table,string - recently changed files + self._changed = {} + +end + +--------------------------------------------------------------------------------------------------- +-- Properties + +function cFileMonitor:get_changed() + return self._changed +end + +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:get_is_monitoring() + return self._is_monitoring +end + +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:get_paths() + return self._paths +end + +function cFileMonitor:set_paths(paths) + self._paths = paths +end + +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:get_polling_interval() + return self.polling_interval_observable.value +end + +function cFileMonitor:set_polling_interval(val) + self.polling_interval_observable.value = val +end + +--------------------------------------------------------------------------------------------------- +-- Public methods +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:start() + TRACE("cFileMonitor:start()") + + -- register idle observable + self:_add_notifier() + + self._is_monitoring.value = true +end + +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:stop() + TRACE("cFileMonitor:stop()") + + -- unregister idle observable + self:_remove_notifier() + + self._is_monitoring.value = false +end + +--------------------------------------------------------------------------------------------------- +-- Private methods +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:idle_notifier() + + -- polling interval + local do_poll = false + if (self.polling_interval > 0) then + local time = os.clock() + if not self._last_poll then + self._last_poll = time + do_poll = true + elseif (self._last_poll + self.polling_interval < time) then + self._last_poll = time + do_poll = true + end + else + do_poll = true + end + + if not do_poll then + return + end + + -- check for changes and memorize + local modified = self:_get_modified() + if not table.is_empty(modified) then + self._changed = {} + for path,stats in pairs(modified) do + self._stats[path] = stats + table.insert(self._changed,path) + end + self.changed_observable:bang() + end + +end + +--------------------------------------------------------------------------------------------------- +-- iterate through, and check all monitored files +-- @return table {filename = stats} or nil if no changed files + +function cFileMonitor:_get_modified() + --TRACE("cFileMonitor:get_modified()") + + local rslt = {} + + for _,path in ipairs(self.paths) do + local stats = self:_get_stats_if_changed(path) + if stats then + rslt[path] = stats + end + end + + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- @param path (string) +-- @return stats (table) if changed or nil if not + +function cFileMonitor:_get_stats_if_changed(path) + --TRACE("cFileMonitor:_get_stats_if_changed(path)",path) + + local cached = self._stats[path] + if cached then + local stats = io.stat(path) + if stats then + if (stats.mtime > cached.mtime) then + return stats + end + end + elseif self.emit_initial then + return io.stat(path) + end + +end + +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:_remove_notifier() + TRACE("cFileMonitor:_remove_notifier()") + + local idle_obs = renoise.tool().app_idle_observable + if idle_obs:has_notifier(self,cFileMonitor.idle_notifier) then + idle_obs:remove_notifier(self,cFileMonitor.idle_notifier) + end + +end + + +--------------------------------------------------------------------------------------------------- + +function cFileMonitor:_add_notifier() + TRACE("cFileMonitor:_add_notifier()") + + local idle_obs = renoise.tool().app_idle_observable + if not idle_obs:has_notifier(self,cFileMonitor.idle_notifier) then + idle_obs:add_notifier(self,cFileMonitor.idle_notifier) + end + +end + + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFilesystem.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFilesystem.lua new file mode 100644 index 00000000..26a7db6b --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cFilesystem.lua @@ -0,0 +1,659 @@ +--[[=============================================================================================== +cFilesystem +===============================================================================================]]-- + +--[[-- + +Static methods for dealing with the file-system +. +# + +]] + +require (_clibroot.."cString") + +class 'cFilesystem' + +cFilesystem.FILETYPE = { + FOLDER = 1, + FILE = 2, +} + +--------------------------------------------------------------------------------------------------- + +function cFilesystem.get_userdata_folder() + TRACE("cFilesystem.get_userdata_folder()") + + local search_path = (os.platform() == "WINDOWS") + and "\\Scripts\\Libraries\\?.lua" + or "/Scripts/Libraries/?.lua" + + local iterator = string.gmatch(package.path,";([^;]+)") + for str in iterator do + if (string.sub(str,-24)==search_path) then + if string.find(str,"Users") -- win 8 + or string.find(str,"Resources") -- unix/osx/win + or string.find(str,"/usr/") -- linux + -- TODO confirm with as many OS as possible + then + return cFilesystem.unixslashes(string.sub(str,0,#str-#search_path)).."/" + end + end + end + +end + + +--------------------------------------------------------------------------------------------------- + +function cFilesystem.get_user_folder() + local bundle_path = renoise.tool().bundle_path + local platform = os.platform() + if (platform == "WINDOWS") then + local offset = string.find(bundle_path,"AppData") + return string.sub(bundle_path,1,offset-1) + elseif (platform == "MACINTOSH") then + local offset = string.find(bundle_path,"Library") + return string.sub(bundle_path,1,offset-1) + elseif (platform == "LINUX") then + local offset = string.find(bundle_path,"%.config") + if offset then + return string.sub(bundle_path,1,offset-1) + else + -- Fallback to environment variable or default + local home = os.getenv("HOME") + if home then + return home .. "/" + else + return "/home/" .. (os.getenv("USER") or "user") .. "/" + end + end + end +end + +--------------------------------------------------------------------------------------------------- + +function cFilesystem.get_resource_folder() + TRACE("cFilesystem.get_resource_folder()") + + local search_path = (os.platform() == "WINDOWS") + and "\\Scripts\\Libraries\\?.lua" + or "/Scripts/Libraries/?.lua" + + local iterator = string.gmatch(package.path,";([^;]+)") + for str in iterator do + if (string.sub(str,-24)==search_path) then + if string.find(str,"Resources") -- win + -- TODO other OS combinations + then + return cFilesystem.unixslashes(string.sub(str,0,#str-#search_path)).."/" + end + end + end + +end + +--------------------------------------------------------------------------------------------------- +-- split path into parts, seperated by slashes +-- important - folders should end with a slash +-- note: this is a virtual function which doesn't require I/O access +-- @param file_path (string) +-- @return string, folder +-- @return string, filename +-- @return string, extension + +function cFilesystem.get_path_parts(file_path) + TRACE("cFilesystem.get_path_parts()") + + cFilesystem.assert_string(file_path,"file_path") + + local patt = "(.-)([^\\/]-%.?([^%.\\/]*))$" + local folder,filename,extension = string.match(file_path,patt) + + if (filename == extension) then + extension = nil + end + + if (filename == "") then + filename = nil + end + + return folder,filename,extension + +end + +--------------------------------------------------------------------------------------------------- +-- provided with a complete path, returns just the filename (no extension) +-- note: this is a virtual function which doesn't require I/O access +-- @param file_path (string) +-- @return string or nil + +function cFilesystem.get_raw_filename(file_path) + TRACE("cFilesystem.get_raw_filename(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + local folder,filename,extension = cFilesystem.get_path_parts(file_path) + if not filename then + return + end + if extension then + return cFilesystem.file_strip_extension(filename,extension) + else + return filename + end + +end + + +--------------------------------------------------------------------------------------------------- +-- check if the given string indicates a root folder +-- note: a root folder is considered "/" on unix-based systems, and +-- [drive letter]:/ on windows systems +-- note: this is a virtual function which doesn't require I/O access + +function cFilesystem.is_root_folder(str) + TRACE("cFilesystem.is_root_folder(str)",str) + + cFilesystem.assert_string(str,"str") + + str = cFilesystem.unixslashes(str) + if (str == "/") then + return true + elseif (str:match("[a-zA-Z]?:/+$")) then + return true + else + return false + end + +end + +--------------------------------------------------------------------------------------------------- +-- provided with a string, this method will find the parent folder +-- note: returned string is using unix slashes +-- note: this is a virtual function which doesn't require I/O access +-- @param file_path (string) +-- @return string or nil if failed +-- @return int, error code + +function cFilesystem.get_parent_directory(file_path) + TRACE("cFilesystem.get_parent_directory(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + local folder,filename,extension = cFilesystem.get_path_parts(file_path) + local path_parts = cFilesystem.get_directories(folder) + if (#path_parts > 1) then + table.remove(path_parts) + return table.concat(path_parts,"/").."/" + else -- root + return file_path + end + +end + +--------------------------------------------------------------------------------------------------- +-- Recursively copy folder +-- @param src_path (string) +-- @param dest_path (string) +-- @param [file_ext] (table), whitelisted file extensions, e.g. {"*.bmp"} +-- @param level (int), how many levels to recurse (undefined or 0 is unlimited) + +function cFilesystem.copy_folder(src_path,dest_path,file_ext,level) + TRACE("cFilesystem.copy_folder(src_path,dest_path,file_ext,level)",src_path,dest_path,file_ext,level) + + cFilesystem.assert_string(src_path,"src_path") + cFilesystem.assert_string(dest_path,"dest_path") + + -- @return false to stop recursion + local callback_fn = function(path,file,type) + --print(">>> callback_fn - path,file,type",path,file,type) + -- find difference between original path and this one, + -- and append that difference to our dest.path + local start_idx,end_idx = path:find(src_path) + if not end_idx then + error("Unexpected error") + end + local target_path = dest_path.."/"..path:sub(end_idx,#path) + if (type == cFilesystem.FILETYPE.FILE) then + local src_fname = path.."/"..file + local dest_fname = target_path.."/"..file + cFilesystem.copy_file(src_fname,dest_fname) + else + os.mkdir(target_path.."/"..file) + end + return true + end + + cFilesystem.recurse(src_path,callback_fn,file_ext,level) + +end + +--------------------------------------------------------------------------------------------------- +-- if file already exist, return a name with (number) appended to it +-- @param file_path (string) + +function cFilesystem.ensure_unique_filename(file_path) + TRACE("cFilesystem.ensure_unique_filename(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + local rslt = file_path + local folder,filename,extension = cFilesystem.get_path_parts(rslt) + + local file_no_ext = extension + and cFilesystem.file_strip_extension(filename,extension) + or filename + + local count = cString.detect_counter_in_str(file_no_ext) + + while (io.exists(rslt)) do + if extension then + rslt = ("%s%s (%d).%s"):format(folder,file_no_ext,count,extension) + else + rslt = ("%s%s (%d)"):format(folder,file_no_ext,count) + end + count = count + 1 + end + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- break a string into directories +-- note: this is a virtual function which doesn't require I/O access +-- @param file_path (string) +-- @return table + +function cFilesystem.get_directories(file_path) + TRACE("cFilesystem.get_directories(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + local matches = string.gmatch(file_path,"(.-)([^\\/])") + local part = "" + local parts = {} + for k,v in matches do + if (k=="") then + part = part..v + else + table.insert(parts,part) + part = v + end + end + table.insert(parts,part) + + return parts + +end + +--------------------------------------------------------------------------------------------------- +-- create a whole folder structure in one go +-- (unlike the standard os.mkdir, which is limited to a single folder) +-- @param file_path (string) +-- @return bool, true when folder(s) were created +-- @return string, error message when failed + +function cFilesystem.makedir(file_path) + TRACE("cFilesystem.makedir(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + local folder_path = cFilesystem.get_path_parts(file_path) + local folders = cString.split(folder_path,"[/\\]") + + local tmp_path = "" + + for k,v in ipairs(folders) do + tmp_path = ("%s%s/"):format(tmp_path,v) + if (v == ".") then + -- relative path, skip "dot" + else + if not io.exists(tmp_path) then + if cFilesystem.is_root_folder(tmp_path) then + else + local success,err = os.mkdir(tmp_path) + if not success then + return false,err + end + end + end + end + end + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- rename a file or folder +-- @param old_f (string) +-- @param new_f (string) +-- TODO @param options (table) +-- "replace" - for existing files/folders +-- "merge" - for existing folders + +function cFilesystem.rename(old_f,new_f) + TRACE("cFilesystem.rename(old_f,new_f)",old_f,new_f) + + cFilesystem.assert_string(old_f,"old_f") + cFilesystem.assert_string(new_f,"new_f") + + local passed,err = os.rename(old_f,new_f) + return passed,err + +end + +--------------------------------------------------------------------------------------------------- +-- on non-posix systems (windows), you can't remove a folder which is not +-- empty - this method will iterate through and delete all files/folders +-- @param folder_path (string) +-- @return bool, true when folder was removed +-- @return string, error message when failed + +function cFilesystem.rmdir(folder_path) + TRACE("cFilesystem.rmdir(folder_path)",folder_path) + + cFilesystem.assert_string(folder_path,"folder_path") + + if not io.exists(folder_path) then + return false,"Folder does not exist" + end + + for __, dirname in pairs(os.dirnames(folder_path)) do + cFilesystem.rmdir(folder_path..dirname.."/") + end + + for __, filename in pairs(os.filenames(folder_path)) do + local success,err = os.remove(folder_path..filename) + if not success then + return false,err + end + end + + local success,err = os.remove(folder_path) + if not success then + return false,err + end + +end + +--------------------------------------------------------------------------------------------------- +-- make sure a file/folder name does not contain anything considered bad +-- (such as special characters or preceding ./ dot-slash combinations) +-- @param file_path (string) +-- @return bool,string + +function cFilesystem.validate_filename(file_path) + TRACE("cFilesystem.validate_filename(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + if (file_path == "") then + return false, "Please enter a valid, non-blank name" + end + + if string.find(file_path,"[:\\/<>?|]") then + return false, "The name contains illegal characters" + end + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- convert windows-style paths to unix-style +-- also: remove doubleslashes +-- @param file_path (string) +-- @return string + +function cFilesystem.unixslashes(file_path) + TRACE("cFilesystem.unixslashes(file_path)",file_path) + + local str = file_path:gsub("\\","/") + return str:gsub("/+","/") + +end + +--------------------------------------------------------------------------------------------------- +--- remove illegal characters (similar to validate, but attempts to fix) +-- @param file_path (string) +-- @return string + +function cFilesystem.sanitize_filename(file_path) + TRACE("cFilesystem.sanitize_filename(file_path)",file_path) + + cFilesystem.assert_string(file_path,"file_path") + + return string.gsub(file_path,"[:\\/<>?|]","") + +end + + +--------------------------------------------------------------------------------------------------- +-- add file extension (if it hasn't already got it) +-- @param file_path (string) +-- @param extension (string), e.g. "bmp" +-- @return string + +function cFilesystem.file_add_extension(file_path,extension) + TRACE("cFilesystem.file_add_extension(file_path,extension)",file_path,extension) + + cFilesystem.assert_string(file_path,"file_path") + cFilesystem.assert_string(extension,"extension") + + local check_against = string.sub(file_path,-#extension) + if ((check_against):lower() == (extension):lower()) then + return file_path + else + return ("%s.%s"):format(file_path,extension) + end + +end + + +--------------------------------------------------------------------------------------------------- +-- remove file extension (if present and matching) +-- @param file_path (string) +-- @param extension (string), e.g. "bmp" +-- @return string + +function cFilesystem.file_strip_extension(file_path,extension) + TRACE("cFilesystem.file_strip_extension(file_path,extension)",file_path,extension) + + cFilesystem.assert_string(file_path,"file_path") + cFilesystem.assert_string(extension,"extension") + + local patt = "(.*)%.([^.]*)$" + local everything_else,ext = string.match(file_path,patt) + + if (string.lower(extension) == string.lower(ext)) then + return everything_else + end + + return file_path + +end + + +--------------------------------------------------------------------------------------------------- +-- so widely used it got it's own function + +function cFilesystem.assert_string(str,str_name) + TRACE("cFilesystem.assert_string(str,str_name)",str,str_name) + + assert(str,"No "..str_name.." specified") + assert(type(str)=="string",str_name..": expected string, got"..type(str)) + +end + +--------------------------------------------------------------------------------------------------- +-- load string from disk + +function cFilesystem.load_string(file_path) + TRACE("cFilesystem.load_string(file_path)",file_path) + + local handle,err = io.open(file_path,"r") + if not handle then + return false,err + end + + local finfo = io.stat(file_path) + if (finfo.type ~= "file") then + handle:close() + return false, "Attempting to load string from a non-file" + end + + local str = handle:read("*a") + if not str then + handle:close() + return false, "Failed to read from file" + end + + handle:close() + return str + +end + +--------------------------------------------------------------------------------------------------- +-- list files in a given folder +-- @param str_path (string) +-- @param file_ext (table), valid file extensions , e.g. {"*.bmp"} +-- @param include_path (bool), when true we append path to string +-- @return table + +function cFilesystem.list_files(str_path,file_ext,include_path) + TRACE("cFilesystem.list_files(str_path,file_ext,include_path)",str_path,file_ext,include_path) + + cFilesystem.assert_string(str_path,"str_path") + + if not file_ext then + file_ext = {"*.*"} + end + + if not io.exists(str_path) then + return false,"Can't list files, path does not exist" + end + + local filenames = os.filenames(str_path,file_ext) + local rslt = {} + for k,v in ipairs(filenames) do + if include_path then + table.insert(rslt,str_path.."/"..v) + else + table.insert(rslt,v) + end + end + + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- save string to disk +-- @param file_path (string) +-- @param str (string) +-- @return bool, true when successful, false when not +-- @return string, error message when failed + +function cFilesystem.write_string_to_file(file_path,str) + TRACE("cFilesystem.write_string_to_file(file_path,str)",file_path,str) + + cFilesystem.assert_string(file_path,"file_path") + + local success = true + + local handle,err = io.open(file_path,"w") + if not handle then + -- often triggered by a folder that does not exist + return false,err + end + if not handle:write(str) then + success = false + end + + handle:close() + + if not success then + return false, "Could not write to file" + else + return true + end + +end + +--------------------------------------------------------------------------------------------------- +-- this will work for small files, but is not recommended on larger ones +-- @param file_in (string) +-- @param file_out (string) +-- @return boolean,string + +function cFilesystem.copy_file(file_in,file_out) + TRACE("cFilesystem.copy_file(file_in,file_out)",file_in,file_out) + + cFilesystem.assert_string(file_in,"file_in") + cFilesystem.assert_string(file_out,"file_out") + + local infile,err = io.open(file_in, "r") + if not infile then return false,err end + + local str = infile:read("*a") + if not str then + infile:close() + return false, "Failed to read from file" + end + + infile:close() + + local outfile = io.open(file_out, "w") + if not outfile then return false,err end + + local success,err = outfile:write(str) + if not success then return false,err end + + outfile:close() + +end + + +--------------------------------------------------------------------------------------------------- +-- Iterate through files and folders and invoke a callback function for each entry +-- @param str_path (string), path to start recursion +-- @param callback_fn (function) return false to stop recursion +-- @param [file_ext] (table), valid file extensions , e.g. {"*.bmp"} +-- @param level (int), how many levels to recurse (undefined or 0 is unlimited) +-- @return boolean, true when finished recursing (including when aborted in callback) +-- @return string, error message when failed (e.g. invalid path) + +function cFilesystem.recurse(str_path,callback_fn,file_ext,level) + TRACE("cFilesystem.recurse(str_path,callback_fn,file_ext,level)",str_path,callback_fn,file_ext,level) + + if not file_ext then + file_ext = {"*.*"} + end + + if not level then + level = 0 + end + + if not io.exists(str_path) then + return false, str_path,"path does not exist, returning..." + end + + local filenames = os.filenames(str_path,file_ext) + for k,v in ipairs(filenames) do + if not callback_fn(str_path,v,cFilesystem.FILETYPE.FILE) then + return + end + end + local dirnames = os.dirnames(str_path) + for k,v in ipairs(dirnames) do + if not callback_fn(str_path,v,cFilesystem.FILETYPE.FOLDER) then + return + end + cFilesystem.recurse(str_path.."/"..v,callback_fn,file_ext,level+1) + end + +end + + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cLib.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cLib.lua new file mode 100644 index 00000000..19ea0618 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cLib.lua @@ -0,0 +1,496 @@ +--[[=============================================================================================== +cLib +===============================================================================================]]-- + +--[[-- + +Contains common methods for working with strings, numbers and tables. + +## + +Note that several classes in the cLib library invokes the LOG/TRACE statement - +therefore, it is recommended that any cLib-powered tool includes this file + +]] + +--================================================================================================= + +require (_clibroot.."cValue") +require (_clibroot.."cNumber") + +--------------------------------------------------------------------------------------------------- + +class 'cLib' + +--- largest possible integer value +cLib.HUGE_INT = 0xFFFFFFFF + +--- placeholder value for nil, storable in table. +cLib.NIL = {} + +--- names of classes that have been loaded +cLib.REQUIRED = {} + +--------------------------------------------------------------------------------------------------- +-- [Static] LOG statement - invokes cLib.log(). +-- you can override this method with your own custom implementation +-- @param ... (vararg) + +function LOG(...) + cLib.log(...) +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Placeholder TRACE statement +-- (include cDebug for the full-featured implementation) +-- @param ... (vararg) + +function TRACE(...) + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Print important messages to the console (errors and warnings) +-- @param ... (vararg) + +function cLib.log(...) + + local args = {...} + local n = select('#', ...) + + local success,err = pcall(function() + local result = "" + for i = 1, n do + result = result .. tostring(args[i]) .. "\t" + end + print (result) + end) + + if not success then + print(...) + end + +end + +--------------------------------------------------------------------------------------------------- +-- alternative to require, which can be used to avoid circular dependencies +-- only require when 'classname' is not already present globally, or in cLib + +function cLib.require(file) + TRACE('cLib.require(file)',file) + + -- check if already present + local success,err = pcall(function() + local class_name = string.match(file,"[^\\/]*$") + tostring(_G[class_name]) + end) + + local is_registered = table.find(cLib.REQUIRED,file) + if not success and not is_registered then + if not is_registered then + table.insert(cLib.REQUIRED,file) + end + require(file) + end + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] For class constructors that use this syntax: +-- local obj = SomeObject{ +-- some_prop = "initial value", +-- other_prop = 42 +-- } +-- @param ... (vararg) +-- @return (likely table, but depends on what you feed it) + +function cLib.unpack_args(...) + local args = {...} + if not args[1] then + return {} + else + return args[1] + end +end + +--------------------------------------------------------------------------------------------------- +-- @param ... (vararg) +-- @return table or nil + +function cLib.pack_args(...) + return (#arg >0) and arg or nil +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Call a class method or function and show/log results +-- Note: currently with a maximum of 8 arguments can be passed (this is a +-- convenience function after all...) +-- @param ... (vararg), [class+function or function] + argument(s) + +function cLib.invoke_task(...) + local fn = select(1,...) + fn(select(2,...),select(3,...),select(4,...),select(5,...), + select(6,...),select(7,...),select(8,...),select(9,...)) +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Turn value descriptor into instance +-- @return cNumber or cValue + +function cLib.create_cvalue(t) + + local val = t.value or t.value_default + if (type(val)=="number") then + return cNumber(t) + else + return cValue(t) + end + +end + +--------------------------------------------------------------------------------------------------- +-- Number methods +--------------------------------------------------------------------------------------------------- +-- [Static] Scale value to a range within a range +-- @param value (number) the value we wish to scale +-- @param in_min (number) +-- @param in_max (number) +-- @param out_min (number) +-- @param out_max (number) +-- @return number + +function cLib.scale_value(value,in_min,in_max,out_min,out_max) + return(((value-in_min)*(out_max/(in_max-in_min)-(out_min/(in_max-in_min))))+out_min) +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Attempt to convert string to a number (strip percent sign) +-- @param str (string), e.g. "33.3%" +-- @return number or (TODO) nil if not able to convert + +function cLib.string_to_percentage(str) + TRACE("cLib.string_to_percentage(str)",str) + return tonumber(string.sub(str,1,#str-1)) +end + + +--------------------------------------------------------------------------------------------------- +--- [Static] Get average of supplied numbers +-- @return number + +function cLib.average(...) + local rslt = 0 + for i=1, #arg do + rslt = rslt+arg[i] + end + return rslt/#arg +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Clamp value - ensure value is within min/max +-- @param value (number) +-- @param min_value (number) +-- @param max_value (number) +-- @return number + +function cLib.clamp_value(value, min_value, max_value) + return value < min_value and min_value or value > max_value and max_value or value +end + +--------------------------------------------------------------------------------------------------- +-- [Static] 'Wrap/rotate' value within specified range +-- (with a range of 64-127, a value of 128 should output 65) + +function cLib.wrap_value(value, min_value, max_value) + return (value-min_value) % (max_value-min_value) + min_value +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Determine the sign of a number +-- TODO distinguish between -0 and 0 +-- @return -1 if negative or 1 if positive + +function cLib.sign(x) + return (x<0 and -1) or 1 +end + +--------------------------------------------------------------------------------------------------- +--- [Static] Inverse logarithmic scaling (exponential) + +function cLib.inv_log_scale(ceiling,val) + return ceiling-cLib.log_scale(ceiling,ceiling-val+1) +end + +--------------------------------------------------------------------------------------------------- +--- logarithmic scaling within a fixed space +-- @param ceiling (number) the upper boundary +-- @param val (number) the value to scale + +function cLib.log_scale(ceiling,val) + return math.log(val)*ceiling/math.log(ceiling) +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Check for whole number, using format() + +function cLib.is_whole_number(n) + return (("%.8f"):format(n-math.floor(n)) == "0.00000000") +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Greatest common divisor + +function cLib.gcd(m,n) + while n ~= 0 do + local q = m + m = n + n = q % n + end + return m +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Least common multiplier (2 args) + +function cLib.lcm(m,n) + return ( m ~= 0 and n ~= 0 ) and m * n / cLib.gcd( m, n ) or 0 +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Find least common multiplier +-- @param t (table), use values in table as argument + +function cLib.least_common(t) + local cm = t[1] + for i=1,#t-1,1 do + cm = cLib.lcm(cm,t[i+1]) + end + return cm +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Round value (from http://lua-users.org/wiki/SimpleRound) +-- @param num (number) + +function cLib.round_value(num) + if num >= 0 then return math.floor(num+.5) + else return math.ceil(num-.5) end +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Round with precision (http://lua-users.org/wiki/SimpleRound) +-- @param num (number) +-- @param idp (number) + +function cLib.round_with_precision(num, idp) + local mult = 10 ^ (idp or 0) + return math.floor(num * mult + 0.5) / mult +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Find fundamental value +-- @return number (fundamental), number (repetitions) + +function cLib.fundamental(num, precision) + local tmp = num + local rep = 0 + while (not cLib.float_compare(tmp,math.floor(tmp),precision)) do + tmp = tmp + num + rep = rep + 1 + end + return math.floor(tmp),rep +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Compare two numbers with variable precision +-- @param val1 +-- @param val2 +-- @param precision, '10000000' is suitable for parameter values +-- @return boolean + +function cLib.float_compare(val1,val2,precision) + val1 = cLib.round_value(val1 * precision) + val2 = cLib.round_value(val2 * precision) + return val1 == val2 +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Return the fractional part of a number +-- @param val +-- @return number + +function cLib.fraction(val) + return val-math.floor(val) +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Find number of hex digits needed to represent a number (e.g. 255 = 2) +-- @param val (int) +-- @return int + +function cLib.get_hex_digits(val) + return 8-#string.match(bit.tohex(val),"0*") +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Take a table and convert into strings - useful e.g. for viewbuilder popup +-- (if table is associative, will use values) +-- @param t (table) +-- @param prefix (string) insert before each entry +-- @param suffix (string) insert after each entry +-- @return table + +function cLib.stringify_table(t,prefix,suffix) + + local rslt = {} + for k,v in ipairs(table.values(t)) do + table.insert(rslt,("%s%s%s"):format(prefix or "",tostring(v),suffix or "")) + end + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Receives a string argument and turn it into a proper object or value +-- @param str (string), e.g. "renoise.song().transport.keyboard_velocity" +-- @return value (can be nil) +-- @return string, error message when failed + +function cLib.parse_str(str) + + local rslt + local success,err = pcall(function() + rslt = loadstring("return " .. str)() + end) + + if success then + return rslt + else + return nil,err + end + +end + +--------------------------------------------------------------------------------------------------- +-- Check if table values are all identical +-- (useful e.g. for detecting if a color is tinted) +-- @return boolean + +function cLib.table_has_equal_values(t) + + local val = nil + for k,v in ipairs(t) do + if (val==nil) then + val = v + end + if (val~=v) then + return false + end + end + return true + +end + +--------------------------------------------------------------------------------------------------- +-- Quick'n'dirty table compare, compares values (not keys) +-- @return boolean, true if identical + +function cLib.table_compare(t1,t2) + return (table.concat(t1,",")==table.concat(t2,",")) +end + +--------------------------------------------------------------------------------------------------- +-- Count table entries, including mixed types +-- TODO replaced by table.count?? +-- @return int or nil + +function cLib.table_count(t) + local n=0 + if ("table" == type(t)) then + for key in pairs(t) do + n = n + 1 + end + return n + else + return nil + end +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Try serializing a value or return "???" +-- the result should be a valid, quotable string +-- @param obj, value or object +-- @return string + +function cLib.serialize_object(obj) + local succeeded, result = pcall(tostring, obj) + if succeeded then + result=string.gsub(result,"\n","\\n") -- return code + result=string.gsub(result,'\\"','\\\\"') -- double-quotes + result=string.gsub(result,'"','\\"') -- single-quotes + return result + else + return "???" + end +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Serialize table into string, with some formatting options +-- @param t (table) +-- @param max_depth (int), determine how many levels to process - optional +-- @param longstring (boolean), use longstring format for multiline text +-- @return table + +function cLib.serialize_table(t,max_depth,longstring) + + assert(type(t) == "table", "this method accepts only a table as argument") + + local rslt = "{\n" + if not max_depth then + max_depth = 9999 + end + + + -- table dump helper + local function rdump(t, indent, depth) + local result = ""--"\n" + indent = indent or string.rep(' ', 2) + depth = depth or 1 + local too_deep = (depth > max_depth) and true or false + local next_indent + -- list keys in alphabetic order + local keys = table.keys(t) + table.sort(keys) + for _, key in ipairs(keys) do + local value = t[key] + local str_key = (type(key) == 'number') and '' or '["'..cLib.serialize_object(key) .. '"] = ' + if (type(value) == 'table') then + if table.is_empty(value) then + result = result .. indent .. str_key .. '{},\n' + elseif too_deep then + result = result .. indent .. str_key .. '"table...",\n' + else + next_indent = next_indent or (indent .. string.rep(' ', 2)) + result = result .. indent .. str_key .. '{\n' + depth = depth + 1 + result = result .. rdump(value, next_indent .. string.rep(' ', 2), depth) + result = result .. indent .. '},\n' + end + else + if longstring and type(value)=="string" and string.find(value,"\n") then + result = result .. indent .. str_key .. '[[' .. value .. ']]' .. ',\n' + else + local str_quote = (type(value) == "string") and '"' or "" + result = result .. indent .. str_key .. str_quote .. cLib.serialize_object(value) .. str_quote .. ',\n' + end + end + end + + return result + end + + rslt = rslt .. rdump(t) .. "}" + return rslt + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cNumber.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cNumber.lua new file mode 100644 index 00000000..6cc0ecb8 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cNumber.lua @@ -0,0 +1,120 @@ +--[[============================================================================ +cNumber +============================================================================]]-- + +--[[-- + +Extend standard number with min/max and polarity. + +## +A good base class for e.g. device parameters, which it is modelled over + +]] + +------------------------------------------------------------------------------- + + +require (_clibroot.."cValue") + +class 'cNumber' (cValue) + +cNumber.POLARITY = { + UNIPOLAR = 1, + BIPOLAR = 2, +} + +------------------------------------------------------------------------------- + +function cNumber:__init(...) + TRACE("cNumber:__init(...)") + + local args = cLib.unpack_args(...) + + --- cNumber.POLARITY + self.polarity = args.polarity or cNumber.POLARITY.UNIPOLAR + + --- number + self.value_min = args.value_min or 0 + + --- number + self.value_max = args.value_max or 1 + + --- number, value of 1 means integer + self.value_quantum = args.value_quantum or nil + + --- table, strings representing enum states + -- (only relevant when integer) + self.value_enums = args.value_enums or nil + + --- number, how to scale value - '1' with factor of 100 becomes '100' + -- this is relevant for some values that are represented differently + -- than their actual value (e.g. 'phrase.shuffle') + self.value_factor = args.value_factor or 1 + + --- function, when defined also define value_tonumber + self.value_tostring = args.value_tostring or nil + + --- function, when defined also define value_tostring + self.value_tonumber = args.value_tonumber or nil + + --- bool, when value starts from zero + -- (it should be used for display purposes only) + self.zero_based = (type(args.zero_based)=="boolean") and args.zero_based or false + + -- initialize -- + + cValue.__init(self,...) + +end + +------------------------------------------------------------------------------- + +function cNumber:add(val) + self:set_value(self._value+val) +end + +function cNumber:subtract(val) + self:set_value(self._value-val) +end + +function cNumber:multiply(val) + self:set_value(self._value*val) +end + +function cNumber:divide(val) + self:set_value(self._value/val) +end + +------------------------------------------------------------------------------- + +function cNumber:set_value(val) + self._value = cLib.clamp_value(val,self.value_min,self.value_max) +end + +------------------------------------------------------------------------------- +-- Meta-methods +------------------------------------------------------------------------------- + +function cNumber:__add(val) + self:add(val) + return self +end + +function cNumber:__sub(val) + self:subtract(val) + return self +end + +function cNumber:__mul(val) + self:multiply(val) + return self +end + +function cNumber:__div(val) + self:divide(val) + return self +end + + + + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cObservable.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cObservable.lua new file mode 100644 index 00000000..d58394e7 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cObservable.lua @@ -0,0 +1,429 @@ +--[[============================================================================ +cObservable +============================================================================]]-- + +--[[-- + +Be 'smart' about observable properties in the Renoise API +. +# + +This class tries to be clever and provide 'inside knowledge' about properties. +For example, the valid range of a "selected_instrument_index" is an integer with a minimum of 1 + +For now, we only care about the "first level" properties - the ones that belong to the song object. + +]] + + +class 'cObservable' + +cObservable.SONG = { + artist_observable = {type="string"}, + name_observable = {type="string"}, + comments_observable = {type="table",subclass="string"}, -- array of strings + show_comments_after_loading_observable = {type="boolean"}, + instruments_observable = {type="table",subclass="Instrument"}, + patterns_observable = {type="table",subclass="Pattern"}, + tracks_observable = {type="table",subclass="Track"}, + selected_instrument_observable = {type="table",subclass="Instrument"}, + selected_instrument_index_observable = {type="number",subclass="integer",min=1}, + selected_phrase_observable = {type="table",subclass="InstrumentPhrase"}, + selected_phrase_index_observable = {type="number",subclass="integer",min=0}, + selected_sample_observable = {type="Sample"}, + selected_sample_modulation_set_observable = {type="SampleModulationSet"}, + selected_sample_device_chain_observable = {type="SampleDeviceChain"}, + selected_sample_device_observable = {type="AudioDevice"}, + selected_track_observable = {type="Track"}, + selected_track_index_observable = {type="number",subclass="integer",min=1}, + selected_track_device_observable = {type="AudioDevice"}, + --selected_device -- deprecated + selected_parameter_observable = {type="DeviceParameter"}, + selected_automation_parameter_observable = {type="DeviceParameter"}, + selected_automation_device_observable = {type="AudioDevice"}, + selected_pattern_observable = {type="Pattern"}, + selected_pattern_index_observable = {type="number",subclass="integer",min=1}, + selected_pattern_track_observable = {type="PatternTrack"}, + selected_sequence_index_observable = {type="number",subclass="integer",min=1}, + selected_note_column_observable = {type="NoteColumn"}, + transport = { + playing_observable = {type="boolean"}, + bpm_observable = {type="number",subclass="float",min=32,max=999}, + lpb_observable = {type="number",subclass="integer",min=1,max=256}, + tpl_observable = {type="number",subclass="integer",min=1,max=16}, + loop_pattern_observable = {type="boolean"}, + edit_mode_observable = {type="boolean"}, + edit_step_observable = {type="number",subclass="integer",min=0,max=64}, + octave_observable = {subclass="integer",min=0,max=8}, + metronome_enabled_observable = {type="boolean"}, + metronome_beats_per_bar_observable = {type="number",subclass="integer",min=1,max=16}, + metronome_lines_per_beat_observable = {type="number",subclass="integer",min=0,max=256}, -- 0 = songs current LPB + metronome_precount_enabled_observable = {type="boolean"}, + metronome_precount_bars_observable = {type="number",subclass="integer",min=1,max=4}, + record_quantize_enabled_observable = {type="boolean"}, + record_quantize_lines_observable = {type="number",subclass="integer",min=1,max=32}, + record_parameter_mode_observable = {enum={ + renoise.Transport.RECORD_PARAMETER_MODE_PATTERN, + renoise.Transport.RECORD_PARAMETER_MODE_AUTOMATION}}, + follow_player_observable = {type="boolean"}, + wrapped_pattern_edit_observable = {type="boolean"}, + single_track_edit_mode_observable = {type="boolean"}, + groove_enabled_observable = {type="boolean"}, + track_headroom_observable = {type="number",subclass="decibel"}, + keyboard_velocity_enabled_observable = {type="boolean"}, + keyboard_velocity_observable = {type="number",subclass="integer",min=0,max=127}, + }, + sequencer = { + --:sequence_is_start_of_section_observable = + --:sequence_section_name_observable + --:sequence_sections_changed_observable + keep_sequence_sorted_observable = {type="boolean"}, + selection_range_observable = {type="table"}, -- array of two numbers + pattern_sequence_observable = {type="table",subclass="integer"}, -- array of numbers + pattern_assignments_observable = {type="Observable"}, -- ?? + pattern_slot_mutes_observable = {type="Observable"}, + }, + +} + +--[[ + +-- refresh when SONG.tracks_observable change + +cObservable.Track = { + prefx_volume -> DeviceParameter + prefx_panning -> DeviceParameter + prefx_width -> DeviceParameter + column_is_muted_observable + column_name_observable + name_observable + color_observable, + color_blend_observable + mute_state_observable + solo_state_observable + collapsed_observable + output_routing_observable + output_delay_observable + visible_effect_columns_observable + visible_note_columns_observable + volume_column_visible + panning_column_visible + delay_column_visible + sample_effects_column_visible_observable + devices__observable +} + +cObservable.AudioDevice = { + display_name_observable + is_active_observable + is_maximized_observable + active_preset_observable +} + +cObservable.DeviceParameter = { + is_automated_observable + is_midi_mapped_observable + show_in_mixer_observable + value_observable + value_string_observable +} + +]] + +-- precomputed version +cObservable.SONG_BY_TYPE = {} + +cObservable.MODE = { + MANUAL = 1, + AUTOMATIC = 2, +} + +cObservable.mode = cObservable.MODE.MANUAL + +------------------------------------------------------------------------------- +-- automatically attach to song (auto-renew registered observables) + +function cObservable.set_mode(mode) + + if (cObservable.mode ~= mode) + and (cObservable.mode == cObservable.MODE.AUTOMATIC) + then + -- remove notifier + end + + if (mode == cObservable.MODE.AUTOMATIC) then + -- add notifier + end + +end + +------------------------------------------------------------------------------- +-- get a specific type of observable +-- @param str_type, string ("boolean","number" or "string") +-- @param array, list of cObservable descriptors +-- @return table or nil + +function cObservable.get_by_type(str_type,array) + + if cObservable.SONG_BY_TYPE[str_type] then + return cObservable.SONG_BY_TYPE[str_type] + end + + if not array then + array = cObservable.SONG + end + + local t = {} + for k,v in pairs(array) do + if type(v) == "table" then + if type(v.type) ~= "nil" and (v.type == str_type) then + t[k] = v + else + t[k] = cObservable.get_by_type(str_type,v) + end + end + end + return not table.is_empty(t) and t or nil + +end + +-- precompute +cObservable.SONG_BY_TYPE["boolean"] = cObservable.get_by_type("boolean") +cObservable.SONG_BY_TYPE["number"] = cObservable.get_by_type("number") +cObservable.SONG_BY_TYPE["string"] = cObservable.get_by_type("string") + +------------------------------------------------------------------------------- +-- combine the above search with a match for a given name +-- @param str_type (string), one of xStreamArg.BASE_TYPES +-- @param str_obs (string), e.g. "transport.keyboard_velocity_enabled_observable" +-- @param str_prefix (string), e.g. "rns." + +function cObservable.get_by_type_and_name(str_type,str_obs,str_prefix) + + -- strip away prefix + if str_prefix then + local s,e = string.find(str_obs,'^'..str_prefix) + if s and e then + str_obs = string.sub(str_obs,e+1) + end + end + + local matches = cObservable.get_by_type(str_type) + + -- break string into segments + local obs_parts = cString.split(str_obs,"%.") + local tmp = matches[obs_parts[1]] + local target = tmp + local count = 1 + while tmp do + count = count + 1 + tmp = tmp[obs_parts[count]] + if tmp then + target = tmp + end + end + + return target or {} + +end + + +------------------------------------------------------------------------------- +-- return a 'flattened' list of observable names, e.g. +-- "transport.keyboard_velocity_enabled_observable" +-- @param str_type (string), one of xStreamArg.BASE_TYPES +-- @param prefix (string), e.g. "rns." +-- @param arr (table) supply observables (when we got them) +-- @return table + +function cObservable.get_keys_by_type(str_type,prefix,arr) + + if not arr then + arr = cObservable.get_by_type(str_type) + end + + if not prefix then + prefix = "" + end + + local t = {} + for k,v in pairs(arr) do + if type(v) == "table" then + if not v.type then + prefix = prefix..k.."." + local branch = cObservable.get_keys_by_type(str_type,prefix,v) + for k2,v2 in ipairs(branch) do + table.insert(t,v2) + end + else + table.insert(t,prefix..k) + end + end + end + + return t + +end + +-------------------------------------------------------------------------------- +-- Remove notifier - wrap in protected call (when observables are gone) +-- supports all three combinations of arguments: +-- function or (object, function) or (function, object) +-- @param obs (renoise.Document.ObservableXXX) +-- @param arg1 (function or object) +-- @param arg2 (function or object) +-- @return bool, true when attached +-- @return string, error message when failed + +function cObservable.detach(obs,arg1,arg2) + + local err + obs,err = cObservable.retrieve_observable(obs) + if err then + return false,err + end + + local passed,err = pcall(function() + if type(arg1)=="function" then + local fn,obj = arg1,arg2 + if obj then + if obs:has_notifier(fn,obj) then + obs:remove_notifier(fn,obj) + end + else + if obs:has_notifier(fn) then + obs:remove_notifier(fn) + end + end + elseif type(arg2)=="function" then + local obj,fn = arg1,arg2 + if obs:has_notifier(obj,fn) then + obs:remove_notifier(obj,fn) + end + else + error("Unsupported arguments") + end + end) + + return passed,err + +end + +-------------------------------------------------------------------------------- +-- Add notifier, while checking for / removing existing one +-- supports all three combinations of arguments: +-- function or (object, function) or (function, object) +-- @param obs (renoise.Document.ObservableXXX) +-- @param arg1 (function or object) +-- @param arg2 (function or object) +-- @return bool, true when attached +-- @return string, error message when failed + +function cObservable.attach(obs,arg1,arg2) + + local err = nil + obs,err = cObservable.retrieve_observable(obs) + if err then + return false,err + end + + cObservable.detach(obs,arg1,arg2) + + if type(arg1)=="function" then + local fn,obj = arg1,arg2 + if obj then + obs:add_notifier(fn,obj) + else + obs:add_notifier(fn) + end + elseif type(arg2)=="function" then + local obj,fn = arg1,arg2 + obs:add_notifier(obj,fn) + else + error("Unsupported arguments") + end + + return true + +end + +-------------------------------------------------------------------------------- +-- support the use of string-based observable names +-- @param obs (string or ObservableXXX) +-- @return ObservableXXX + +function cObservable.retrieve_observable(obs) + + local err + if (type(obs)=="string") then + obs,err = cLib.parse_str(obs) + if err then + return false,err + end + end + return obs + +end + +-------------------------------------------------------------------------------- +-- remove all entries from ObservableXXXList with specified value + +function cObservable.list_remove(obs,val) + TRACE("cObservable.list_remove",obs,val) + + for k = 1,#obs do + if obs[k] and (val == obs[k].value) then + obs:remove(k) + end + end + return obs + +end + +-------------------------------------------------------------------------------- +-- add to ObservableXXXList when not already present + +function cObservable.list_add(obs,val) + TRACE("cObservable.list_add",obs,val) + + local exists = false + for k = 1,#obs do + if obs[k] and (val == obs[k].value) then + exists = true + end + end + if not exists then + obs:insert(val) + end + return obs + +end + +-------------------------------------------------------------------------------- +-- return table containing all names (using .dot syntax) + +function cObservable.get_song_names(prefix) + + local rslt = {} + local branches = {"transport","sequencer"} + + if not prefix then + prefix = "rns." + end + + for k,v in pairs(cObservable.SONG) do + if not table.find(branches,k) then + table.insert(rslt,prefix..k) + end + end + + for k,v in pairs(branches) do + for k2,v2 in pairs(cObservable.SONG[v]) do + table.insert(rslt,prefix..v.."."..k2) + end + end + + table.sort(rslt) + return rslt + +end diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cParseXML.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cParseXML.lua new file mode 100644 index 00000000..9c5ba2c9 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cParseXML.lua @@ -0,0 +1,97 @@ +--[[============================================================================ +cParseXML +============================================================================]]-- +--[[ + +Ability to parse xml into a lua table +. +# + +Based on/requires the slaxdom parser +TODO implement more xpath-alike methods + +]] + +class 'cParseXML' + +SLAXML = require (_clibroot.."/support/slaxdom/slaxml") +SLAXML = require (_clibroot.."/support/slaxdom/slaxdom") + +------------------------------------------------------------------------------- +--- load and parse XML from disk +-- @return table or nil + +function cParseXML.load_and_parse(file_path) + TRACE('cParseXML.load_and_parse(file_path)',file_path) + + local str_xml = io.open(file_path):read('*all') + return cParseXML.parse(str_xml) + +end + +------------------------------------------------------------------------------- +--- parse XML from string +-- @return table or nil + +function cParseXML.parse(str_xml) + TRACE('cParseXML.parse(str_xml)',str_xml) + + local doc = SLAXML:dom(str_xml,{ simple=true,stripWhitespace=true }) + + return doc + +end + +------------------------------------------------------------------------------- +--- retrieve named attribute +-- @return table or nil + +function cParseXML.get_attribute(doc,attr_name) + TRACE('cParseXML.get_attribute(doc,attr_name)',doc,attr_name) + + if table.is_empty(doc) then + return + end + + if not table.is_empty(doc.kids) then + for k,v in ipairs(doc.kids) do + if (v.name == attr_name) then + return v + end + end + end + +end + +------------------------------------------------------------------------------- +--- retrieve property by path +-- @return table or nil + +function cParseXML.get_node_by_path(doc,xpath) + TRACE("cParseXML.get_node_by_path(doc,xpath)",doc,xpath) + + local parts = cString.split(xpath,"/") + local node = doc + for k,v in ipairs(parts) do + node = cParseXML.get_attribute(node,v) + end + return node + +end + +------------------------------------------------------------------------------- +-- retrieve value of property +-- @return string or nil + +function cParseXML.get_node_value(node) + TRACE("cParseXML.get_node_value(node)",node) + + if not node then + return + end + if node.kids[1] then + return node.kids[1].value + end + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPersistence.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPersistence.lua new file mode 100644 index 00000000..df7f1471 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPersistence.lua @@ -0,0 +1,296 @@ +--[[=============================================================================================== +-- cPersistence +===============================================================================================]]-- + +--[[-- + +Add the ability to store a class as serialized data +. + +# How to use + +First, extend your class, e.g. +``` +class 'MyClass' (cPersistence) +``` + +Next, choose how to/which properties to persist + + Method 1: Automatic (let cPersistence do the work) + + With this approach, you need to define a special __PERSISTENCE property. + MyClass.__PERSISTENCE = {"foo","bar"} + + Method 2: Define how to obtain and assign properties manually + + Specify (override) the following methods: + cPersistence:obtain_definition() + cPersistence:assign_definition() + +Finally, you can override the serialize method as well, in case you want to +customize the resulting string. + + +--]] + +--================================================================================================= + +require (_clibroot.."cTable") +require (_clibroot.."cReflection") + +class 'cPersistence' + +--------------------------------------------------------------------------------------------------- +-- load serialized string from disk +-- @return boolean, true when loading succeeded +-- @return string, when an error occurred + +function cPersistence:load(file_path) + TRACE("cPersistence:load(file_path)") + + assert(type(file_path)=="string") + + -- confirm that file is valid + local str_def,err = cFilesystem.load_string(file_path) + --print(">>> load_definition - load_string - str_def,err",str_def,err) + local passed = self:looks_like_definition(str_def) + if not passed then + return false,("The file '%s' does not look like a definition"):format(file_path) + end + + -- load the definition + local passed,err = pcall(function() + assert(loadfile(file_path)) + end) + if not passed then + err = "*** Error: Failed to load the definition '"..file_path.."' - "..err + return false,err + end + + local def = assert(loadfile(file_path))() + self:assign_definition(def) + +end + +--------------------------------------------------------------------------------------------------- +-- save serialized string to disk +-- @return boolean, true when loading succeeded +-- @return string, when an error occurred + +function cPersistence:save(file_path) + TRACE("cPersistence:save(file_path)",file_path) + + assert(type(file_path)=="string") + + local got_saved,err = cFilesystem.write_string_to_file(file_path,self:serialize()) + if not got_saved then + return false,err + end + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- @return string + +function cPersistence:serialize() + TRACE("cPersistence:serialize()") + + return cLib.serialize_table(self:obtain_definition()) + +end + +--------------------------------------------------------------------------------------------------- +-- assign definition to class +-- @param def (table) +-- @param ref (object), where to assign values - 'self' if undefined +-- @param _prop_names (table), + +function cPersistence:assign_definition(def,_ref,_prop_names) + TRACE("cPersistence:assign_definition(def,_ref,_prop_names)",def,_ref,_prop_names) + + assert(type(def)=="table") + + -- assign to persisted object + -- (first check if the type is available in global scope) + local create_class_instance = function(def,cname) + --print(">>> create_class_instance",def,cname) + if not rawget(_G,cname) then + renoise.app():show_warning( + ("Could not instantiate: unknown class '%s'"):format(cname)) + else + local ref = _G[cname]() + ref:assign_definition(def) + return ref + end + end + + -- check if persisted object? note: only possible for nested entries, + -- we can't change the fundamental type of class from within + local cname = cPersistence.get_persisted_type(def) + --print("cname",cname) + if _ref and cname then + return create_class_instance(def,cname) + end + + -- defined when recursing + _ref = _ref and _ref or self + _prop_names = _prop_names and _prop_names or self.__PERSISTENCE + --print("_ref,_prop_names",_ref,rprint(_prop_names)) + + for _,prop_name in ipairs(_prop_names) do + --print(">>> assign_definition - prop_name",prop_name) + local prop_def = def[prop_name] + local cname = cPersistence.get_persisted_type(prop_def) + if cname then + --print(">>> looks like a persisted object",prop_name,cname) + _ref[prop_name] = create_class_instance(prop_def,cname) + else + if (type(prop_def)=="table" and cTable.is_indexed(prop_def)) then + --print(">>> table assignment",prop_name,prop_def) + _ref[prop_name] = {} + for k,v in ipairs(prop_def) do + -- pass an empty table as reference - this indicates that we are recursing + -- also, when recursing make sure we stay within the cPersistence scope + -- (a class that have extended this one might have overridden the method) + local table_item_def = {} + table_item_def = cPersistence.assign_definition(self,v,table_item_def,table.keys(v)) + --print("table_item_def",table_item_def) + table.insert(_ref[prop_name],table_item_def) + end + else + --print(">>> plain assignment",prop_name,prop_def) + _ref[prop_name] = prop_def + end + end + end + + return _ref + +end + +--------------------------------------------------------------------------------------------------- +-- look for certain "things" to confirm that this is a valid definition +-- @param str_def (string) +-- @return bool + +function cPersistence:looks_like_definition(str_def) + TRACE("cPersistence:looks_like_definition(str_def)",str_def) + + assert(type(str_def)=="string") + + local pre = '\[?\"?' + local post = '\]?\"?[%s]*=[%s]' + + for _,prop_name in ipairs(self.__PERSISTENCE) do + if not string.find(str_def,pre..prop_name..post) then + return false + end + end + return true + +end + +--------------------------------------------------------------------------------------------------- +-- obtain a (serializable) table representation of the class +-- note: override this method to define your own implementation +-- @return table + +function cPersistence:obtain_definition() + TRACE("cPersistence:obtain_definition()") + + -- core properties (always present) + local def = { + __type = type(self), + } + + for _,prop_name in ipairs(self.__PERSISTENCE) do + local prop_def = cPersistence.obtain_property_definition(self[prop_name],prop_name) + if prop_def then + def[prop_name] = prop_def + end + end + return def + +end + +--------------------------------------------------------------------------------------------------- + +function cPersistence.obtain_property_definition(prop,prop_name) + TRACE("cPersistence.obtain_property_definition(prop,prop_name)",prop,prop_name) + + local def = {} + if cReflection.is_serializable_type(prop) then + if (type(prop)=="table") then + -- distinguish between indexed and associative tables + if cTable.is_indexed(prop) then + -- make sure to take a recursive copy + --def[prop_name] = table.rcopy(prop) + --def = table.rcopy(prop) + + for k,v in ipairs(prop) do + local table_prop_def = cPersistence.obtain_property_definition(v,k) + if table_prop_def then + def[k] = table_prop_def + end + end + + else + -- associative array ("object") + for k,v in pairs(prop) do + local table_prop_def = cPersistence.obtain_property_definition(v,k) + if table_prop_def then + def[k] = table_prop_def + end + end + end + else + -- primitive value (bool, string, number) + def = prop + end + else + -- check if instance of cPersistence + if prop.__PERSISTENCE and prop.obtain_definition then + def = prop:obtain_definition() + else + LOG("Warning: this property is not serializable:",prop_name) + end + end + return def + +end + +--------------------------------------------------------------------------------------------------- +-- check if the provided definition refers to a persisted object +-- @param def (table) +-- @return string or nil + +function cPersistence.get_persisted_type(def) + --TRACE("cPersistence:get_persisted_type(def)",def) + + return type(def)=="table" and def.__type + +end + +--------------------------------------------------------------------------------------------------- +-- attempt to determine type from first occurrence of '__type' in the file +-- @return string or nil + +function cPersistence.determine_type(fpath) + TRACE("cPersistence.determine_type(fpath)",fpath) + + assert(type(fpath)=="string") + + local str_def,err = cFilesystem.load_string(fpath) + if err then + return false,err + end + + local first = string.find(str_def,"__type") + local match = string.match(str_def.sub(str_def,first),' = "(%a+)"') + --print(match) + + return match + +end diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPreferences.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPreferences.lua new file mode 100644 index 00000000..917c6074 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cPreferences.lua @@ -0,0 +1,660 @@ +--[[=============================================================================================== +-- cPreferences +============================================================================]]-- + +--[[-- + +Maintain multiple preferences for a tool and switch between them. + +## + +Each profile is basically a copy of the preferences.xml stored in the +special 'profiles' folder. + +Note: cPreferences stores it's own settings in the root of the this folder + + +]] + +--================================================================================================= + +require (_clibroot.."cFilesystem") + +class 'cPreferences' + +cPreferences.PROFILES_ENABLED = false +cPreferences.ALWAYS_CHOOSE = false + +cPreferences.DEFAULT_NAME = "Untitled Profile" + +cPreferences.PROFILE_FOLDER = "./profiles/" +cPreferences.BUTTON_H = 26 + +--------------------------------------------------------------------------------------------------- + +function cPreferences:__init(...) + + local args = cLib.unpack_args(...) + + --- string, supply to make the launch dialog feel more familiar + self.tool_name = args.tool_name or "Tool Name" + + --- string, provide name if preferences is based on a class + self.doc_class_name = args.doc_class_name + + --- number of active instances (read-only) + self.active_instances = property(self.get_active_instances) + + --- number, the profile we're running + self.selected_profile_index = nil + + --- table + self.selected_profile = property(self.get_selected_profile) + + --== callbacks ==-- + + --- function, deliver a custom profile to the tool + -- @param doc, renoise.DocumentNode + self.launch_callback = args.launch_callback + + --- function, ask the tool to load standard prefs + self.default_callback = args.default_callback + + --- function, ask the tool not to start + self.abort_callback = args.abort_callback + + --== self-managed ==-- + + --- boolean, if true: detect active instances and choose a profile + -- (an active instance is a profile launched within the cutoff time) + -- if false: use the preferences.xml in the bundle path + -- (in other words, act as a normal tool) + self.profiles_enabled = property(self.get_profiles_enabled,self.set_profiles_enabled) + self.profiles_enabled_observable = renoise.Document.ObservableBoolean(cPreferences.PROFILES_ENABLED) + + --- boolean, if true we always show the chooser + self.always_choose = property(self.get_always_choose,self.set_always_choose) + self.always_choose_observable = renoise.Document.ObservableBoolean(cPreferences.ALWAYS_CHOOSE) + + --- string, the profile to recall on startup + self.recall_profile = property(self.get_recall_profile,self.set_recall_profile) + self.recall_profile_observable = renoise.Document.ObservableString("") + + --== internal ==-- + + --- table + self.profiles = {} + + --- renoise.Dialog + self.dialog = nil + + --- renoise.Views.View + self.dialog_contents = nil + + --- boolean + self.suppress_saving = false + + --== initialize ==-- + + self:load_settings() + self:scan_profiles() + self:build_dialog() + +end + +--------------------------------------------------------------------------------------------------- +-- Getter/Setters +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_selected_profile() + return self.profiles[self.selected_profile_index] +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_profiles_enabled() + return self.profiles_enabled_observable.value +end + +function cPreferences:set_profiles_enabled(val) + self.profiles_enabled_observable.value = val + self:save_settings() +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_always_choose() + return self.always_choose_observable.value +end + +function cPreferences:set_always_choose(val) + self.always_choose_observable.value = val + self:save_settings() +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_recall_profile() + return self.recall_profile_observable.value +end + +function cPreferences:set_recall_profile(val) + self.recall_profile_observable.value = val + self:save_settings() +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_active_instances() + local count = 0 + for k,v in ipairs(self.profiles) do + if (v.active) then + count = count + 1 + end + end + return count + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_profile_names() + + local rslt = {} + for k,v in ipairs(self.profiles) do + table.insert(rslt,v.name) + end + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- Class Methods +--------------------------------------------------------------------------------------------------- +-- determine which profiles are active/available +-- + create folders when missing + +function cPreferences:scan_profiles() + + self.profiles = {} + + local str_path = cPreferences.PROFILE_FOLDER + + if not io.exists(str_path) then + os.mkdir(str_path) + end + + local dirnames = os.dirnames(str_path) + for k,v in ipairs(dirnames) do + local filenames = os.filenames(str_path..v) + local has_config,active,mtime = false,false,nil + for k2,v2 in ipairs(filenames) do + local filestats = io.stat(str_path..v.."/"..v2) + + if (v2 == "preferences.xml") then + has_config = true + end + if (v2 == "active") then + active = true + mtime = filestats.mtime + end + end + table.insert(self.profiles,{ + name = v, + has_config = has_config, + active = active, + mtime = mtime, + }) + end + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:get_profile_by_name(str_name) + + for k,v in ipairs(self.profiles) do + if (v.name == str_name) then + return v,k + end + end + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:attempt_launch() + + local profile,profile_idx = self:get_profile_by_name(self.recall_profile) + + if self.profiles_enabled then + if profile then + self:launch_profile(profile_idx) + elseif self.always_choose then + self:show_dialog() + return + end + end + + self.default_callback() + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:close_dialog() + + if (self.dialog and self.dialog.visible) then + self.dialog:close() + end + self.dialog = nil + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:launch_profile(idx) + + local profile = self.profiles[idx] + if profile then + + -- instantiate as class or scriptingtool prefs? + local doc + if self.doc_class_name then + doc = _G[self.doc_class_name]() + else + doc = renoise.Document.create("ScriptingToolPreferences"){} + end + + local prefs_path = cPreferences.PROFILE_FOLDER.."/"..profile.name.."/preferences.xml" + doc:load_from(prefs_path) + + -- backup existing preferences + local tool_prefs_from = renoise.tool().bundle_path.."preferences.xml" + local tool_prefs_to = renoise.tool().bundle_path.."preferences.xml.old" + os.move(tool_prefs_from,tool_prefs_to) + + -- create lock file + local lock_path = cPreferences.PROFILE_FOLDER.."/"..profile.name.."/active" + local lock_str = "This file indicates that the profile is in use" + cFilesystem.write_string_to_file(lock_path,lock_str) + + self.selected_profile_index = idx + self.launch_callback(doc) + + end + +end + +--------------------------------------------------------------------------------------------------- +-- save preferences for the selected profile +-- @return boolean,string + +function cPreferences:remove_profile(idx) + + local profile = self.profiles[idx] + if not profile then + return false, "Can't remove, profile doesn't exist" + end + + local str_path = cPreferences.PROFILE_FOLDER.."/"..profile.name.."/" + local success,err = cFilesystem.rmdir(str_path) + if not success then + return false,err + end + + table.remove(self.profiles,idx) + if (idx == self.selected_profile_index) then + self.selected_profile_index = nil + end + +end + + +--------------------------------------------------------------------------------------------------- +-- save preferences for the selected profile +-- @return boolean,string + +function cPreferences:add_profile(str_name) + + local str_path = cPreferences.PROFILE_FOLDER.."/"..str_name + local str_path = cFilesystem.ensure_unique_filename(str_path) + local suggested_name = cFilesystem.get_raw_filename(str_path) + + -- create folder and empty set of preferences + os.mkdir(str_path) + local file_out = str_path .."/preferences.xml" + + local doc + if self.doc_class_name then + doc = _G[self.doc_class_name]() + else + doc = renoise.Document.create("ScriptingToolPreferences"){} + end + doc:save_as(file_out) + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- save preferences for the selected profile +-- @return boolean,string + +function cPreferences:rename_profile(idx,str_name) + + local profile = self.profiles[idx] + if not profile then + return false, "Can't rename, profile doesn't exist" + end + + local str_path = cPreferences.PROFILE_FOLDER.."/"..str_name + local str_path = cFilesystem.ensure_unique_filename(str_path) + local suggested_name = cFilesystem.get_raw_filename(str_path) + + if (str_name ~= suggested_name) then + return false,"A profile already exist with that name, please choose another one" + end + + local str_path_old = cPreferences.PROFILE_FOLDER.."/"..profile.name + cFilesystem.rename(str_path_old,str_path) + + return true + +end + + +--------------------------------------------------------------------------------------------------- +-- save preferences for the selected profile +-- @return boolean,string + +function cPreferences:update_profile() + + local profile = self.selected_profile + if not profile then + return false,"Can't update, no profile is selected" + end + local doc = renoise.tool().preferences + + local prefs_path = cPreferences.PROFILE_FOLDER.."/"..profile.name.."/preferences.xml" + local passed,err = doc:save_as(prefs_path) + if not passed then + return false,err + end + + return true + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:show_dialog() + + if (not self.dialog or not self.dialog.visible) then + self.dialog = renoise.app():show_custom_dialog( + ("%s - Select Profile"):format(self.tool_name), self.dialog_contents) + else + self.dialog:show() + end + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:rebuild_and_show() + + self:close_dialog() + self:scan_profiles() + self:build_dialog() + self:show_dialog() + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:build_dialog() + + local vb = renoise.ViewBuilder() + + -- show session time as "XX minutes ago" when + -- time difference is less than one hour... + local human_time_display = function(mtime) + local time_diff = os.difftime(os.time(),mtime) + if (time_diff < 3600) then + return ("%d minutes ago"):format(time_diff/60) + else + return os.date("%c",mtime) + end + end + + local items = {} + for k,v in ipairs(self.profiles) do + local mtime_display = human_time_display(v.mtime) + local str_suffix = "" + if v.active then + str_suffix = (" - Last session was %s"):format(mtime_display) or "" + elseif not v.has_config then + str_suffix = " - using default settings" + end + table.insert(items,v.name..str_suffix) + end + + local vb_submit_buttons = vb:row{ + vb:button{ + text = "Proceed", + height = cPreferences.BUTTON_H, + notifier = function() + if not vb.views.profile_chooser then + self.default_callback() + else + local idx = vb.views.profile_chooser.value + if (idx == 1) then + self.default_callback() + else + self:launch_profile(idx-1) + end + end + self:close_dialog() + end, + }, + vb:button{ + text = "Add...", + height = cPreferences.BUTTON_H, + notifier = function() + local str_name = cPreferences.DEFAULT_NAME + str_name = vPrompt.prompt_for_string(str_name,"Enter name","Add Profile") + if not str_name then + return + end + local success,err = self:add_profile(str_name) + if err then + renoise.app():show_warning(err) + else + self:rebuild_and_show() + end + end, + }, + vb:button{ + id = "xprefs_remove_bt", + text = "Remove", + active = false, + visible = not table.is_empty(items) and true or false, + height = cPreferences.BUTTON_H, + notifier = function() + local msg = "Are you sure you want to remove this profile?" + local choice = renoise.app():show_prompt("Remove Profile",msg,{"OK","Cancel"}) + if (choice == "OK") then + local idx = vb.views.profile_chooser.value + local success,err = self:remove_profile(idx-1) + if err then + renoise.app():show_warning(err) + else + self:rebuild_and_show() + end + end + end, + }, + vb:button{ + id = "xprefs_rename_bt", + text = "Rename", + active = false, + visible = not table.is_empty(items) and true or false, + height = cPreferences.BUTTON_H, + notifier = function() + local idx = vb.views.profile_chooser.value + local str_name = self.profiles[idx-1].name + str_name = vPrompt.prompt_for_string(str_name,"Enter name","Rename Profile") + local success,err = self:rename_profile(idx-1,str_name) + if err then + renoise.app():show_warning(err) + else + self:rebuild_and_show() + end + + end, + }, + vb:button{ + text = "Don't Launch", + height = cPreferences.BUTTON_H, + notifier = function() + if self.abort_callback then + self.abort_callback() + end + self:close_dialog() + + end, + }, + + } + + if table.is_empty(items) then + self.dialog_contents = vb:column{ + margin = 6, + spacing = 6, + vb:row{ + vb:text{ + text = ("%s supports configuration profiles," + .."\nbut no profiles have yet been defined." + .."\n" + .."\nClick 'Proceed' to launch with current settings," + .."\nor 'Add Profile' to create a new profile."):format(self.tool_name), + }, + }, + vb:row{ + vb:checkbox{ + value = not self.always_choose, + notifier = function(val) + self.always_choose = not val + end + }, + vb:text{ + text = "Do not show this dialog" + }, + }, + vb_submit_buttons, + } + + else + table.insert(items,1,"Launch tool with current settings") + local profile_index = 1 + if (self.recall_profile ~="") then + local tmp_idx = table.find(items,self.recall_profile) + if tmp_idx then + profile_index = tmp_idx + end + end + self.dialog_contents = vb:column{ + margin = 6, + spacing = 6, + vb:row{ + vb:text{ + text = ("%s supports configuration profiles - " + .."\nplease select one before launching:"):format(self.tool_name), + }, + }, + vb:row{ + margin = 6, + vb:chooser{ + id = "profile_chooser", + items = items, + value = profile_index, + notifier = function(idx) + if (idx == 1) then + vb.views.xprefs_remove_bt.active = false + vb.views.xprefs_rename_bt.active = false + else + vb.views.xprefs_remove_bt.active = true + vb.views.xprefs_rename_bt.active = true + end + end, + }, + }, + vb:row{ + vb:checkbox{ + value = not self.always_choose, + notifier = function(val) + self.always_choose = not val + if val then + local idx = vb.views.profile_chooser.value + if (idx == 1) then + self.recall_profile = "" + else + local profile = self.profiles[idx-1] + self.recall_profile = profile.name + end + else + self.recall_profile = "" + end + end + }, + vb:text{ + text = "Remember this choice" + }, + }, + + vb_submit_buttons, + } + + end + + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:load_settings() + + local doc = renoise.Document.create("cPreferencesSettings"){} + doc:add_property("profiles_enabled", renoise.Document.ObservableBoolean(cPreferences.PROFILES_ENABLED)) + doc:add_property("always_choose", renoise.Document.ObservableBoolean(cPreferences.ALWAYS_CHOOSE)) + doc:add_property("recall_profile", renoise.Document.ObservableString("")) + + local success,err = doc:load_from(cPreferences.PROFILE_FOLDER.."settings.xml") + if success then + self.suppress_saving = true + self.profiles_enabled = doc:property('profiles_enabled').value + self.always_choose = doc:property('always_choose').value + self.recall_profile = doc:property('recall_profile').value + self.suppress_saving = false + end + + +end + +--------------------------------------------------------------------------------------------------- + +function cPreferences:save_settings() + + if self.suppress_saving then + return + end + + local doc = renoise.Document.create("cPreferencesSettings"){} + doc:add_property("profiles_enabled", self.profiles_enabled) + doc:add_property("always_choose", self.always_choose) + doc:add_property("recall_profile", renoise.Document.ObservableString(self.recall_profile)) + + local success,err = doc:save_as(cPreferences.PROFILE_FOLDER.."settings.xml") + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cProcessSlicer.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cProcessSlicer.lua new file mode 100644 index 00000000..85a56926 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cProcessSlicer.lua @@ -0,0 +1,127 @@ +--[[============================================================================ +process_slicer.lua +============================================================================]]-- + +--[[-- + +ProcessSlicer allows you to slice up a function which takes a lot of +processing time into multiple calls via Lua coroutines. + +### Example usage: + + local slicer = ProcessSlicer(my_process_func, argument1, argumentX) + +This starts calling 'my_process_func' in idle, passing all arguments +you've specified in the ProcessSlicer constructor. + + slicer:start() + +To abort a running sliced process, you can call "stop" at any time +from within your processing function of outside of it in the main thread. +As soon as your process function returns, the slicer is automatically +stopped. + + slicer:stop() + +To give processing time back to Renoise, call 'coroutine.yield()' +anywhere in your process function to temporarily yield back to Renoise: + + function my_process_func() + for j=1,100 do + -- do something that needs a lot of time, and periodically call + -- "coroutine.yield()" to give processing time back to Renoise. Renoise + -- will switch back to this point of the function as soon as has done + -- "its" job: + coroutine.yield() + end + end + + +### Drawbacks: + +By slicing your processing function, you will also slice any changes that are +done to the Renoise song into multiple undo actions (one action per slice/yield). + +Modal dialogs will block the slicer, cause on_idle notifications are not fired then. +It will even block your own process GUI when trying to show it modal. + + +]] + +-------------------------------------------------------------------------------- +-- @param process_func (function) +-- @param VarArg (...) + +class "ProcessSlicer" + +function ProcessSlicer:__init(process_func, ...) + assert(type(process_func) == "function", + "expected a function as first argument") + + self.__process_func = process_func + self.__process_func_args = arg + self.__process_thread = nil +end + + +-------------------------------------------------------------------------------- +--- @return true when the current process currently is running + +function ProcessSlicer:running() + return (self.__process_thread ~= nil) +end + + +-------------------------------------------------------------------------------- +--- start a process + +function ProcessSlicer:start() + assert(not self:running(), "process already running") + + self.__process_thread = coroutine.create(self.__process_func) + + renoise.tool().app_idle_observable:add_notifier( + ProcessSlicer.__on_idle, self) +end + + +-------------------------------------------------------------------------------- +--- stop a running process + +function ProcessSlicer:stop() + assert(self:running(), "process not running") + + renoise.tool().app_idle_observable:remove_notifier( + ProcessSlicer.__on_idle, self) + + self.__process_thread = nil +end + + +-------------------------------------------------------------------------------- +--- function that gets called from Renoise to do idle stuff. switches back +-- into the processing function or detaches the thread + +function ProcessSlicer:__on_idle() + assert(self.__process_thread ~= nil, "ProcessSlicer internal error: ".. + "expected no idle call with no thread running") + + -- continue or start the process while its still active + if (coroutine.status(self.__process_thread) == 'suspended') then + local succeeded, error_message = coroutine.resume( + self.__process_thread, unpack(self.__process_func_args)) + + if (not succeeded) then + -- stop the process on errors + self:stop() + -- and forward the error to the main thread + error(error_message) + end + + -- stop when the process function completed + elseif (coroutine.status(self.__process_thread) == 'dead') then + self:stop() + end +end + + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cReflection.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cReflection.lua new file mode 100644 index 00000000..12d38894 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cReflection.lua @@ -0,0 +1,245 @@ +--[[=============================================================================================== +cReflection +===============================================================================================]]-- + +--[[-- + +Pull off some API tricks to achieve reflection-alike abilities. + +## + +]] + +--================================================================================================= + +class 'cReflection' + +--------------------------------------------------------------------------------------------------- +-- Copy properties from one class instance to another +-- @param from_class (userdata) +-- @param to_class (userdata) +-- @param level (int) for internal use +-- @return bool, true when copied + +function cReflection.copy_object_properties(from_class,to_class,level) + TRACE("cReflection.copy_object_properties(from_class,to_class,level)",from_class,to_class,level) + + if (type(from_class) ~= type(to_class))then + LOG("*** Classes need to be of an identical type:", + type(from_class),type(to_class)) + return false + end + + local copy_property = function(val,target_class,prop_name) + target_class[prop_name] = val + end + + local level = level or 0 + local max_level = 1 + local props = cReflection.get_object_info(from_class) + for _,prop in ipairs(props) do + if (prop:find("_observable")) then + -- skip observables + elseif not cReflection.is_standard_type(from_class[prop]) then + if (level < max_level) then + cReflection.copy_object_properties(from_class[prop],to_class[prop],level+1) + end + else + --props_table[prop] = c[prop] + local success = pcall(copy_property,from_class[prop],to_class,prop) + if not success then + end + end + end + + return true + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] cast a value to boolean, provide fallback value if undefined +-- (can be used for setting arguments) + +function cReflection.as_boolean(val,fallback) + if (type(val)=="nil") then + return fallback + else + return cReflection.cast_value(val,"boolean") + end +end + +--------------------------------------------------------------------------------------------------- +-- cast variable as basic datatype (boolean,number,string) + +function cReflection.cast_value(val,val_type) + TRACE("cReflection.cast_value(val,val_type)",val,val_type) + + if (val_type == "boolean") then + if (type(val) == "boolean") then + return val + elseif (type(val) == "string") then + if (val == "true") or (val == "1") then + return true + else + return false + end + elseif (type(val) == "number") then + if (val == 1) then + return true + else + return false + end + else + error("Could not cast value as boolean") + end + + elseif (val_type == "string") then + return tostring(val) + elseif (val_type == "number") then + return tonumber(val) + else + error("Unsupported datatype") + end + +end + +--------------------------------------------------------------------------------------------------- +-- get properties from class instance +-- @param class (userdata) +-- @return table + +function cReflection.get_object_properties(class,_level) + TRACE("cReflection.get_object_properties(class,_level)",class,_level) + + local props_table = {} + local level = _level or 0 + local max_level = 1 + local props = cReflection.get_object_info(class) + for _,prop in ipairs(props) do + if (prop:find("_observable")) then + -- skip observables + --print("skipped observable property",prop) + elseif not cReflection.is_standard_type(class[prop]) then + if (level < max_level) then + props_table[prop] = cReflection.get_object_properties(class[prop],level+1) + end + else + props_table[prop] = class[prop] + end + end + + return props_table + +end + +--------------------------------------------------------------------------------------------------- +-- get native object properties (renoise API) +-- @return table or nil + +function cReflection.get_object_info(class) + TRACE("cReflection.get_object_info(class)",class) + + local rslt = {} + local str = objinfo(class) + local begin1,begin2 = str:find("properties:") + if begin2 then + local end1 = str:find("methods:") + local capture = str:sub(begin2+1,end1-1) + for prop in capture:gmatch("([%a_]+)") do + table.insert(rslt,prop) + end + end + return rslt +end + +--------------------------------------------------------------------------------------------------- +-- @param val (any) +-- @return boolean + +function cReflection.is_standard_type(val) + + return table.find({ + "nil", + "boolean", + "number", + "string", + "table", + "function", + "thread" + },type(val)) + +end + +--------------------------------------------------------------------------------------------------- +-- check if a given value is serializable +-- @param val (any) +-- @return boolean + +function cReflection.is_serializable_type(val) + + return table.find({ + "boolean", + "number", + "string", + "table", + },type(val)) + +end + +--------------------------------------------------------------------------------------------------- +-- evaluate string, assign value to the resulting object +-- @param str (string), e.g. "renoise.song().transport.keyboard_velocity" +-- @param value (vararg), any basic lua type +-- @return boolean, string (error message when failed) + +function cReflection.set_property(str,value) + + -- wrap strings in quotes + value = (type(value)=="string") and '"'..value..'"' or value + + local success,err = pcall(function() + loadstring(str .. " = " .. tostring(value))() + end) + + if not success then + return false,err + else + return true + end + +end + +--------------------------------------------------------------------------------------------------- +-- attempt to evaluate expression in string +-- TODO run in sandbox +-- @return number or nil + +function cReflection.evaluate_string(x) + TRACE("cReflection.evaluate_string(x)",x) + local num + local x_str = 'return '..x + if (pcall(loadstring(x_str)) == false or loadstring(x_str)()==nil) then + return nil + else + num=loadstring(x_str)() + end + return tonumber(num) +end + +--------------------------------------------------------------------------------------------------- +-- @param str (string), name of indentifier +-- @return boolean, string (error message when failed) + +function cReflection.is_valid_identifier(str) + + if string.match(str,"^%d+") then + return false, ("'%s' is not a valid identifier (avoid using number as the first character)"):format(str) + end + local match = string.match(str,"[_%w]*") + if match and (#match == #str) then + return true + else + return false, ("'%s' is not a valid identifier (avoid using special characters)"):format(str) + end + +end diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cSandbox.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cSandbox.lua new file mode 100644 index 00000000..7ec18945 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cSandbox.lua @@ -0,0 +1,377 @@ +--[[============================================================================ +cSandbox +============================================================================]]-- + +--[[-- + +cSandbox allows you to execute code in a controlled environment. + +# + +## How to use + + -- create instance + sandbox = cSandbox() + + -- supply function + sandbox.callback_str = + "print('hello world')" + + -- and call it... + sandback.callback() + + +## Arguments and return values + + -- if you need to supply arguments and define return value, + -- one approach is to define a 'prefix' and 'suffix' + -- (always added to the generated code) + + -- define arguments + sandbox.str_prefix = "local some_arg = select(1, ...)" + + -- define return value + sandbox.str_suffix = "return some_arg" + + -- supply function + sandbox.callback_str = + "some_arg = some_arg + 'foo'".. + "print(some_arg)" + + -- now call the function like this: + local result = sandback.callback('my_arg') + + +## Custom properties + +TODO how to ... + +]] + +require (_clibroot.."cString") + +class 'cSandbox' + +-- disallow the following lua methods/properties +cSandbox.UNTRUSTED = { + "collectgarbage", + "coroutine", + "dofile", + "io", + "load", + "loadfile", + "module", + "os", + "setfenv", + "class", + "rawset", + "rawget", +} + +function cSandbox:__init() + + + --- function, compiled function (when set, always valid) + self.callback = nil + + --- string, code to insert into all generated functions + self.str_prefix = "" + self.str_suffix = "return" + + --- boolean, set to true for instant compilation of callback string + self.compile_at_once = true + + --- string, text representation of the function + self.callback_str = property(self.get_callback_str,self.set_callback_str) + self.callback_str_observable = renoise.Document.ObservableString("") + + --- properties can contain custom get/set methods + self.properties = property(self.get_properties,self.set_properties) + self._properties = {} + + --- invoked when callback has changed + self.modified_observable = renoise.Document.ObservableBang() + + --- table, sandbox environment + self.env = nil + + -- initialize -- + + local env = { + assert = _G.assert, + error = _G.error, + ipairs = _G.ipairs, + loadstring = _G.loadstring, + math = _G.math, + next = _G.next, + pairs = _G.pairs, + print = _G.print, + pcall = _G.pcall, + select = _G.select, + string = _G.string, + table = _G.table, + tonumber = _G.tonumber, + tostring = _G.tostring, + type = _G.type, + unpack = _G.unpack, + -- renoise extended + ripairs = _G.ripairs, + rprint = _G.rprint, + oprint = _G.oprint, + } + + self.env = env + env = {} + + -- metatable (constants and shorthands) + setmetatable(self.env,{ + __index = function (t,k) + if table.find(cSandbox.UNTRUSTED,k) then + error("Property or method is not allowed:"..k) + else + if self.properties[k] and self.properties[k].access then + return self.properties[k].access(env) + else + return env[k] + end + end + end, + __newindex = function (t,k,v) + if self.properties[k] and self.properties[k].assign then + self.properties[k].assign(env,v) + else + env[k] = v + end + + end, + __metatable = false -- prevent tampering + }) + + +end + +--============================================================================== +-- Getter/Setter Methods +--============================================================================== + +function cSandbox:get_callback_str() + --TRACE("cSandbox:get_callback_str - ",self.callback_str_observable.value) + return self.callback_str_observable.value +end + +-- it's recommended to call test_syntax before setting this value, +-- otherwise you get no feedback if it failed + +function cSandbox:set_callback_str(str_fn) + TRACE("cSandbox:set_callback_str(str_fn)",#str_fn) + + assert(type(str_fn) == "string", "Expected string as parameter") + + local modified = (str_fn ~= self.callback_str_observable.value) + + local str_combined = self:prepare_callback(str_fn) + local passed,err = self:test_syntax(str_combined) + + self.callback_str_observable.value = str_fn + + if not err and self.compile_at_once then + local passed,err = self:compile() + if not passed then -- should not happen! + LOG(err) + end + else + LOG(err) + end + + if modified then + self.modified_observable:bang() + end + +end + +------------------------------------------------------------------------------- + +function cSandbox:get_properties() + return self._properties +end + +function cSandbox:set_properties(val) + assert(type(val) == "table", "Expected table as parameter") + self._properties = val +end + + +--============================================================================== +-- Class Methods +--============================================================================== +-- wrap callback in function with variable run-time arguments +-- @param str_fn (string) function as string + +function cSandbox:prepare_callback(str_fn) + --TRACE("cSandbox:prepare_callback(str_fn)",#str_fn) + + assert(type(str_fn) == "string", "Expected string as parameter") + + local str_combined = [[return function(...) + ]]..self.str_prefix..[[ + ]].."\n"..str_fn.."\n"..[[ + ]]..self.str_suffix..[[ + end]] + + return str_combined + +end + + +------------------------------------------------------------------------------- +-- call method to evaluate function (ensure that it's safe to run) +-- @return boolean, true when method passed +-- @return string, error message when failed + +function cSandbox:compile() + + if (self.callback_str == "") then + return true,"cSandbox: no function was provided" + end + + local str_combined = self:prepare_callback(self.callback_str) + local def,err = loadstring(str_combined) + if not def then + return false,err + end + self.callback = def() + setfenv(self.callback, self.env) + + return true + +end + +------------------------------------------------------------------------------- +-- nested block comments/longstrings are depricated and will fail + +function cSandbox.contains_comment_blocks(str) + TRACE("cSandbox.contains_comment_blocks(str)") + + if string.find(str,"%[%[") then + return true + elseif string.find(str,"%]%]") then + return true + else + return false + end + +end + + +------------------------------------------------------------------------------- +-- check for syntax errors +-- (assert provides better error messages) +-- @param str_fn (string) function as string +-- @return boolean, true when passed +-- @return string, when failed + +function cSandbox:test_syntax(str_fn) + TRACE("cSandbox:test_syntax(str_fn)",str_fn) + + local function untrusted_fn() + assert(loadstring(str_fn)) + end + setfenv(untrusted_fn, self.env) + local pass,err = pcall(untrusted_fn) + if not pass then + return false,err + end + + return true + +end + +------------------------------------------------------------------------------- +-- strip code comments from a string +-- @param str_fn (string) +-- @return string + +function cSandbox.strip_comments(str_fn) + TRACE("cSandbox.strip_comments(str_fn)",str_fn) + + local t = cString.split(str_fn,"\n") + for k,v in ripairs(t) do + local ln = cString.trim(v) + if (ln:sub(0,2) == "--") then + table.remove(t,k) + end + end + return table.concat(t,"\n") + +end + +------------------------------------------------------------------------------- +-- check if a given string consists of comments only +-- @param str_fn (string) +-- @return bool + +function cSandbox.contains_code(str_fn) + TRACE("cSandbox.contains_code(str_fn)",str_fn) + + return string.match(cSandbox.strip_comments(str_fn),"%a") and true or false + +end + +------------------------------------------------------------------------------- +-- automatically insert a return statement into a code snippet +-- step 1: detect if return statement is present + +function cSandbox.insert_return(str_fn) + TRACE("cSandbox.insert_return(str_fn)",str_fn) + + local present = false + local t = cString.split(str_fn,"\n") + for k,v in ipairs(t) do + if not present then + local ln = cString.trim(v) + + if (ln ~= "") then -- skip empty lines + if (ln:sub(0,2) ~= "--") then -- skip initial comment blocks + if (ln ~= "-") then -- single minus (can be the result + -- of commenting out, live coding style...) + present = true + -- only insert if not already present + if (ln:sub(0,6) ~= "return") then + t[k] = ("return %s"):format(ln) + end + end + end + end + + end + end + + return table.concat(t,"\n") + +end + +------------------------------------------------------------------------------- +-- "safer" renaming of a string token (for example, a variable name) +-- @param str_fn (string), the function text +-- @param old_name (string) +-- @param new_name (string) +-- @param prefix (string), prepend to old/new name when defined + +function cSandbox.rename_string_token(str_fn,old_name,new_name,prefix) + + local str_search = prefix and prefix..old_name or old_name + local str_replace = prefix and prefix..new_name or new_name + local str_patt = "(.?)("..str_search..")([^%w])" + str_fn = string.gsub(str_fn,str_patt,function(...) + local c1,c2,c3 = select(1,...),select(2,...),select(3,...) + local patt = "[%w_]" + if string.match(c1,patt) or string.match(c3,patt) then + return c1..c2..c3 + end + return c1..str_replace..c3 + end) + + return str_fn + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cScheduler.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cScheduler.lua new file mode 100644 index 00000000..94cbed4f --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cScheduler.lua @@ -0,0 +1,148 @@ +--[[=============================================================================================== +cScheduler +===============================================================================================]]-- + +--[[-- + +Schedule tasks to execute after a defined amount of time. + +## About + +## Changelog + +1.04 +- Dynamically add/detach idle notifier as the need arises + + +]]-- + +--================================================================================================= + +class 'cScheduler' + +-------------------------------------------------------------------------------- + +--- Initialize the cScheduler class + +function cScheduler:__init() + TRACE("cScheduler:__init()") + + self.tasks = table.create() + +end + + +-------------------------------------------------------------------------------- + +--- Perform idle task (check when it's time to execute a task) + +function cScheduler:on_idle() + + -- check time + for idx,task in ripairs(self.tasks) do + if (task.time<=os.clock()) then + self:_execute_task(task) + self.tasks:remove(idx) + end + end +end + + +-------------------------------------------------------------------------------- + +--- Add a new task to the scheduler +-- @param ref (Object) the object to use as context (optional) +-- @param func (func) the function to call +-- @param delay (number) the delay before executing task +-- @param ... (Vararg) variable number of extra arguments + +function cScheduler:add_task(ref,func,delay, ...) + TRACE("cScheduler:add_task()",ref,func,delay) + + local task = cScheduledTask(ref,func,delay,arg) + self.tasks:insert(task) + + local obs = renoise.tool().app_idle_observable + local fn = cScheduler.on_idle + if not obs:has_notifier(self,fn) then + obs:add_notifier(self,fn) + end + + return task + +end + + +-------------------------------------------------------------------------------- + +--- Remove a previously scheduled task +-- @param ref (cScheduledTask) reference to the task + +function cScheduler:remove_task(ref) + TRACE("cScheduler:remove_task()",ref) + + -- remove from list + for idx,task in ripairs(self.tasks) do + if (ref==task) then + self.tasks:remove(idx) + return + end + end + + if (#self.tasks == 0) then + local obs = renoise.tool().app_idle_observable + local fn = cScheduler.on_idle + if obs:has_notifier(self,fn) then + obs:remove_notifier(self,fn) + end + end + +end + + +-------------------------------------------------------------------------------- + +--- Execute a given task (using the provided context or anonymously) +-- @param task (cScheduledTask) reference to the task + +function cScheduler:_execute_task(task) + TRACE("cScheduler:_execute_task",task) + + if task.ref then + task.func(task.ref,unpack(task.args)) + else + task.func(unpack(task.args)) + end +end + + +--[[---------------------------------------------------------------------------- +-- cScheduledTask +----------------------------------------------------------------------------]]-- + +class 'cScheduledTask' + +-------------------------------------------------------------------------------- + +--- A class representing a scheduled task +-- @param ref (Object) the object to use as context (optional) +-- @param func (func) the function to call +-- @param delay (number) the delay before executing task +-- @param args (table) variable number of extra arguments + +function cScheduledTask:__init(ref, func, delay, args) + TRACE("cScheduledTask:__init", ref, func, delay, args) + + self.time = os.clock()+delay + self.args = args + self.ref = ref + self.func = func + +end + + +function cScheduledTask:__eq(other) + return rawequal(self, other) +end + + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cString.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cString.lua new file mode 100644 index 00000000..7a7167ae --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cString.lua @@ -0,0 +1,238 @@ +--[[============================================================================ +cString +============================================================================]]-- + +--[[-- + +Common string-manipulation methods +. +# + +]] + +class 'cString' + +-------------------------------------------------------------------------------- +-- split string - original script: http://lua-users.org/wiki/SplitJoin +-- @param str (string) +-- @param pat (string) pattern +-- @return table + +function cString.split(str, pat) + TRACE("cString.split(str, pat)",str, pat) + + local t = {} -- NOTE: use {n = 0} in Lua-5.0 + local fpat = "(.-)" .. pat + local last_end = 1 + local s, e, cap = str:find(fpat, 1) + while s do + if s ~= 1 or cap ~= "" then + table.insert(t,cap) + end + last_end = e+1 + s, e, cap = str:find(fpat, last_end) + end + if last_end <= #str then + cap = str:sub(last_end) + table.insert(t, cap) + end + return t + +end + +------------------------------------------------------------------------------- +-- remove trailing and leading whitespace from string. +-- http://en.wikipedia.org/wiki/Trim_(8programming) +-- @param s (string) +-- @return string + +function cString.trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +------------------------------------------------------------------------------- +-- capitalize first letter of every word +-- @param s (string) +-- @return string + +function cString.capitalize(s) + return string.gsub(" "..s, "%W%l", string.upper):sub(2) +end + +------------------------------------------------------------------------------- +-- insert return code whenever we encounter dashes or spaces in a string +-- TODO keep dashes, and allow a certain length per line +-- @param str (string) +-- @return string + +function cString.soft_wrap(str) + + local t = cString.split(str,"[-%s]") + return table.concat(t,"\n") + +end + +------------------------------------------------------------------------------- +-- detect counter in string (used for incrementing, unique names) + +function cString.detect_counter_in_str(str) + local count = string.match(str,"%((%d)%)$") + if count then + str = string.gsub(str,"%s*%(%d%)$","") + else + count = 1 + end + return count +end + +------------------------------------------------------------------------------- +-- prepare a string so it can be stored in XML attributes +-- (strip illegal characters instead of trying to fix them) +-- @param str (string) +-- @return string + +function cString.sanitize_string(str) + str=str:gsub('"','') + str=str:gsub("'",'') + return str +end + +-------------------------------------------------------------------------------- +-- sortable time - date which can be sorted alphabetically +-- @return string + +function cString.get_sortable_time(tstamp) + + --[[ + [day] => 22 + [hour] => 19 + [isdst] => true + [min] => 25 + [month] => 5 + [sec] => 58 + [wday] => 6 + [yday] => 142 + [year] => 2015 + ]] + local t = os.date("*t",tstamp) + return ("%d/%.2d/%.2d %.2d:%.2d"):format( + t.year,t.month,t.day,t.hour,t.min) + +end + +--------------------------------------------------------------------------------------------------- +-- format "beat" in the same way as Renoise does it +-- e.g: [beat:line:fraction] +-- @param val (number), +-- @return string + +function cString.format_beat(val) + TRACE("cString.format_beat(val)",val) + + local line = cLib.fraction(val)*(40/10) + local fract = cLib.fraction(line)*256 + return ("%d.%d.%d"):format(math.floor(val),math.floor(line),fract) + +end + +-------------------------------------------------------------------------------- +-- Strip line matching pattern, from multiline string +-- @param str (string) +-- @param patt (string) +-- @return str, resulting string +-- @return int, #stripped lines + +function cString.strip_line(str,patt) + TRACE("cString.strip_line(str,patt)",str,patt) + + local rslt = table.create() + local captures = string.gmatch(str,"([^\n]*)\n") + local line_count = 0 + for k in captures do + if not (string.match(k,patt)) then + rslt:insert(k) + end + line_count = line_count+1 + end + + local lines_stripped = line_count-#rslt + return table.concat(rslt,"\n"),lines_stripped + +end + +-------------------------------------------------------------------------------- +-- Strip leading and/or trailing character from string +-- @param str (string) the string to search +-- @param chr (string) the character to match, e.g. "\n" or " " +-- @param rlead (bool) remove leading +-- @param rtrail (bool) remove trailing +-- @return str + +function cString.strip_leading_trailing_chars(str,chr,rlead,rtrail) + + if rlead then + while (string.sub(str,1,1)==chr) do + str = string.sub(str,2,#str) + end + end + + if rtrail then + while (string.sub(str,#str,#str)==chr) do + str = string.sub(str,1,#str-1) + end + end + + return str + +end + + +------------------------------------------------------------------------------- +-- present a lua table as a formatted string +-- @param t (table) +-- @param args (table) formatting directives +-- multiline: if false, create single-line string +-- number_format: formatting for numeric values (e.g. precision) +-- @return string + +function cString.table_to_string(t,args) + TRACE("cString.table_to_string(t,args)",t,args) + + args = args or {} + args.multiline = args.multiline or false + args.number_format = args.number_format or "%f" + + local str = "" + local linebr = args.multiline and "\n" or "" + + if table.is_empty(t) then + return "{}" + else + str = str.."{" + for k,v in ipairs(t) do + if (type(v) == "string") then + str = ("%s'%s',"):format(str,v) + elseif (type(v) == "number") then + str = ("%s"..args.number_format..","):format(str,v) + elseif (type(v) == "table") then + str = str.."{" + for k2,v2 in pairs(v) do + if (type(v2) == "string") then + local val = (type(v2) == "string") and("'%s'"):format(v2) or v2 + str = ("%s%s = %s,"):format(str,k2,val) + elseif (type(v2) == "number") then + str = ("%s%s = "..args.number_format..","):format(str,k2,v2) + elseif (type(v2) == "boolean") then + str = ("%s%s = %s,"):format(str,k2,(v2) and "true" or "false") + end + end + str = str.."},"..linebr + end + end + str = str.."}"..linebr + end + + return str + +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cTable.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cTable.lua new file mode 100644 index 00000000..b8b85c13 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cTable.lua @@ -0,0 +1,235 @@ +--[[=============================================================================================== +cTable +===============================================================================================]]-- + +--[[-- + +Contains common methods for working with tables + +]] + +--================================================================================================= + +class 'cTable' + +--------------------------------------------------------------------------------------------------- +-- [Static] return the sorted values of the provided table +-- @param t table +-- @return + +function cTable.values(t) + + local rslt = table.values(t) + table.sort(rslt) + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] return the last entry of the provided table +-- NB: only works with indexed tables +-- @param table +-- @return + +function cTable.last(t) + + return t[#t] + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Merge two tables into one (recursive) +-- @param t1 (table) +-- @param t2 (table) +-- @return table + +function cTable.merge(t1,t2) + for k,v in pairs(t2) do + if type(v) == "table" then + if type(t1[k] or false) == "table" then + cTable.merge(t1[k] or {}, t2[k] or {}) + else + t1[k] = v + end + else + t1[k] = v + end + end + return t1 +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Convert a sparsely populated table into a compact one +-- @param t (table) +-- @return table + +function cTable.compact(t) + + if table.is_empty(t) then + return t + end + + local cols = table.keys(t) + table.sort(cols) + for k,v in ipairs(cols) do + t[k] = t[v] + end + local low,high = cTable.bounds(t) + for k = high,#cols+1,-1 do + t[k] = nil + end + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Quick'n'dirty table compare (values in first level only) +-- @param t1 (table) +-- @param t2 (table) +-- @return boolean, true if identical + +function cTable.compare(t1,t2) + return (table.concat(t1,",")==table.concat(t2,",")) +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Match item(s) in an associative array (provide key) +-- @param t (table) +-- @param key (string) +-- @return table + +function cTable.match_key(t,key) + + local rslt = table.create() + for _,v in pairs(t) do + rslt:insert(v[key]) + end + return rslt + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] find nearest value in table of numbers +-- @param t (table) +-- @param val (number) +-- @return number,number (value,key) + +function cTable.nearest(t,val) + TRACE("cTable.nearest(t,val)",#t,val) + + -- sort, but don't modify original table + local vals = table.values(t) + table.sort(vals) + + local prev,key + for k,v in ipairs(vals) do + if (v == val) then + return v,k + end + if (v > val) then + -- return first (lowest) + if not prev then + return v,k + end + -- shortest distance to prev/curr + local d_prev = math.abs(prev-val) + local d_curr = math.abs(v-val) + if (math.min(d_prev,d_curr) == d_prev) then + return prev,k + else + return v,k + end + end + prev = v + key = k + end + + return prev,key + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] return next (higher value) in table of numbers +-- @param t (table) +-- @param val (number) or nil + +function cTable.next(t,val) + TRACE("cTable.next(t,val)",#t,val) + local vals = table.values(t) + table.sort(vals) + local _,idx = cTable.nearest(vals,val) + --print("next - nearest idx",idx,rprint(vals)) + if idx then + return vals[idx+1] + end +end + +--------------------------------------------------------------------------------------------------- +-- [Static] return previous (lower value) in table of numbers +-- @param t (table) +-- @param val (number) or nil + +function cTable.previous(t,val) + TRACE("cTable.previous(t,val)",#t,val) + local vals = table.values(t) + table.sort(vals) + local _,idx = cTable.nearest(vals,val) + --print("previous - nearest idx",idx) + if idx then + return vals[idx-1] + end +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Expand a multi-dimensional array with given keys +-- @param t (table) +-- @param k1 (string) +-- @param k2 (string) +-- @param k3 (string) +-- @param k4 (string) +-- @return table + +function cTable.expand(t,k1,k2,k3,k4) + --TRACE("cTable.expand(t,k1,k2,k3,k4)",t,k1,k2,k3,k4) + + if not t[k1] then + t[k1] = {} + end + if k2 then + t = cTable.expand(t[k1],k2,k3,k4) + end + + return t + +end + +--------------------------------------------------------------------------------------------------- +-- [Static] Find the highest/lowest numeric key (index) in a sparsely populated table +-- @return lowest,highest + +function cTable.bounds(t) + + local lowest,highest = nil,nil + for k,v in ipairs(table.keys(t)) do + if (type(v)=="number") then + if not highest then highest = v end + if not lowest then lowest = v end + highest = math.max(highest,v) + lowest = math.min(lowest,v) + end + end + return lowest,highest + +end + +--------------------------------------------------------------------------------------------------- +-- Check if a given table is indexed (exclusively with numerical indices) + +function cTable.is_indexed(t) + local i = 0 + for _ in pairs(t) do + i = i + 1 + if t[i] == nil then return false end + end + return true +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cValue.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cValue.lua new file mode 100644 index 00000000..cf6e5120 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cValue.lua @@ -0,0 +1,89 @@ +--[[============================================================================ +cValue +============================================================================]]-- + +--[[-- + +Abstract value class +. +# + +]] + +------------------------------------------------------------------------------- + +class 'cValue' + +------------------------------------------------------------------------------- + +function cValue:__init(...) + + local args = cLib.unpack_args(...) + + if (type(args.value_default)=="nil") + and (type(args.value)=="nil") + then + error("cValue needs value_default and/or value to be defined") + end + + --- (boolean/string/number) + self.value_default = args.value_default + or (type(args.value) == "boolean") and false + or (type(args.value) == "string") and "" + or (type(args.value) == "number") and 0 + + --- (boolean/string/number) + self.value = property(self.get_value,self.set_value) + + if (type(args.value)=="boolean") then + self._value = args.value + else + self._value = args.value or self.value_default + end + + +end + +------------------------------------------------------------------------------- + +function cValue:get_value() + return self._value +end + +function cValue:set_value(val) + self._value = val +end + +------------------------------------------------------------------------------- +-- Meta-methods +------------------------------------------------------------------------------- +-- not available + +--function cValue:__index() +--end + +--function cValue:__newindex() +--end + +------------------------------------------------------------------------------- +-- when accessing via paranthesis () + +function cValue:__call(key) + --TRACE("cValue:__call",key) + + if not key then + return self._value + else + assert(type(key)=="string") + return self[key] + end + +end + +------------------------------------------------------------------------------- +-- get length (implement for strings, tables) + +function cValue:__len() + --print("cValue:__len") +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cWaveform.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cWaveform.lua new file mode 100644 index 00000000..c87936c3 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/cWaveform.lua @@ -0,0 +1,707 @@ +--[[=============================================================================================== +cWaveform +===============================================================================================]]-- + +--[[-- + +Static methods for generating waveforms + +]] + +--================================================================================================= + +class 'cWaveform' + +--cWaveform.random_seed = 0 + +cWaveform.x_pre = 0 +cWaveform.x_next = 0 + +cWaveform.brown_parameter = (1/6) + +cWaveform.FORM = { + SIN = 1, + SAW = 2, + SQUARE = 3, + TRIANGLE = 4, + WHITE_NOISE = 5, + BROWN_NOISE = 6, + VIOLET_NOISE = 7, + COPY = 8, +} + +--------------------------------------------------------------------------------------------------- +-- Waveform generators +--------------------------------------------------------------------------------------------------- +-- sine wave function +function cWaveform.sin_2pi_fn(x) + return math.sin(x*2*math.pi) +end +-- cosine wave function +function cWaveform.cos_fn(x) + return math.cos(x*2*math.pi) +end +-- saw wave function +function cWaveform.saw_fn(x) + return 2*(x - math.floor(x + (1/2))) +end +-- Square wave function (op = pulse width) +function cWaveform.square_fn(x) + local x = cWaveform.cycle_fmod(x) + if 0 <= x and x<(1/2) then + return 1 + elseif (1/2) <= x and x < 1 then + return -1 + end + return 0 +end +-- triangle wave function +function cWaveform.triangle_fn(x) + local x = cWaveform.cycle_fmod(x) + if 0 <= x and x <(1/4) then + return 4*x + elseif (1/4)<= x and x < (3/4) then + return -4*x +2 + elseif (3/4)<= x and x <=1 then + return 4*x -4 + end + return 0 +end + +-- white noise +function cWaveform.white_noise_fn() + local y = math.random() + return 2*y -1 +end + +-- brown noise +function cWaveform.brown_noise_fn() + local r = (2*math.random() -1) * cWaveform.brown_parameter -- [-1,1] variable. default 1/6 + cWaveform.x_next = cWaveform.x_pre + r + if cWaveform.x_next > 1 then + cWaveform.x_next = 1 + elseif cWaveform.x_next < -1 then + cWaveform.x_next = -1 + end + cWaveform.x_pre = cWaveform.x_next + return cWaveform.x_next +end + +-- violet noise +function cWaveform.violet_noise_fn() + local r = (2*math.random()-1) + cWaveform.x_next = (r - cWaveform.x_pre)/2 + cWaveform.x_pre = r + return cWaveform.x_next +end + +-- pink noise +function cWaveform.pink_noise_fn() + local r = (2*math.random() -1) + *(1/100) -- [-1,1] variable. + local tmax = math.modf(biased_noise() *5) + for t = 1,tmax+1 do + cWaveform.x_next = cWaveform.x_pre + r + cWaveform.x_pre = cWaveform.x_next + end + if cWaveform.x_next >1 then + cWaveform.x_next = 1 + elseif cWaveform.x_next < -1 then + cWaveform.x_next = -1 + end + return cWaveform.x_next +end + +-- Pink noise(not finished) +-- The Voss-McCartney algorithm +-- http://www.firstpr.com.au/dsp/pink-noise/ +--[[ +function cWaveform.biased_noise() + local r, r1, r2 = nil, math.random(), math.random() + if r2 <= r1 then r = r1 + elseif r2 > r1 then r = (1- r1) + end + return r +end +]] + +--------------------------------------------------------------------------------------------------- + +function cWaveform.mix_fn_fn(fn1,fn2,deg) + local d = (1/2) + if type(deg) == 'number' then d = deg end + return function(x,ch) + --print("fn1",x,ch,fn1(x,ch)*d) + --print("fn2",x,ch,fn2(x,ch)*(1-d)) + return fn1(x,ch)*d + fn2(x,ch)*(1-d) + end +end + +--------------------------------------------------------------------------------------------------- + +function cWaveform.band_limited_fn_fn( + form,cycle,shift,duty_onoff, + duty_fiducal,duty_var,duty_var_frq,range) + + if not duty_onoff then + duty_fiducal,duty_var,duty_var_frq = 50,0,1 + end + local fn,mod_fn + + if (form == cWaveform.FORM.SIN) then + fn = cWaveform.sin_2pi_fn + mod_fn = cWaveform.cycle_phase_duty_mod(cycle,shift,duty_fiducal,duty_var,duty_var_frq) + return fn,mod_fn + end + + local partition_duty_mod = function() + return function(x) + local xx = cycle*x + (shift) + local xxx = duty_fiducal + duty_var*(1/2)*(-1*cWaveform.cos_fn(duty_var_frq*x)+1) + xxx = cWaveform._duty_shape(xxx,range) + return (math.floor(xx) + cWaveform._duty_phase(math.fmod(xx,1),xxx))/cycle + end + end + + local tbl ={{},{}} + local _fn,_mod_fn + _fn = cWaveform._blit_duty_fn_fn(form,range/cycle,duty_fiducal,range) + _mod_fn = partition_duty_mod(cycle,0,duty_fiducal,duty_var,duty_var_frq,range) + fn = function(x) + return _fn(_mod_fn(x)) + end + if (shift ~= 0) then + local p = math.floor(range*shift/cycle) + for i = 1,range -p do + tbl[1][i] = fn((i-1+p)/range) + end + for j = range-p+1,range+1 do + tbl[1][j] = fn((j-1+p-range)/range) + end + fn = cWaveform.table2fn(tbl) + end + + local maximize_fn_fn = function (fn,m,a) + if (a == nil) then + a = 1 + end + local max = 1/32767 + for i = 1,m do + local y = math.abs(fn((i-1)/m)) + if (y >= max) then + max = y + end + end + local aa = 1/max + return function (x) + return aa*a* fn(x) + end + end + + fn = maximize_fn_fn(fn,range,0.95) + return fn,mod_fn + +end + +--------------------------------------------------------------------------------------------------- +-- Utility for changing modulate-function +-- @return function or nil + +function cWaveform.mod_fn_fn(cycle,shift,duty_onoff,duty,duty_var,duty_var_frq) + if not duty_onoff then + return cWaveform.cycle_phase_mod(cycle,shift) + else + return cWaveform.cycle_phase_duty_mod(cycle,shift,duty,duty_var,duty_var_frq) + end +end + + +--------------------------------------------------------------------------------------------------- +-- change wave_fn & mod_fn +-- @return function + +function cWaveform.wave_fn( + form,cycle,shift,duty_onoff, + duty,duty_var,duty_var_frq,band_limited,range) + + TRACE("cWaveform.wave_fn() - ",form,cycle,shift,duty_onoff,duty,duty_var,duty_var_frq,band_limited,range) + + local fn + local mod = cWaveform.mod_fn_fn(cycle,shift,duty_onoff,duty,duty_var,duty_var_frq) + + if (form == cWaveform.FORM.WHITE_NOISE) then + return cWaveform.white_noise_fn + elseif (form == cWaveform.FORM.BROWN_NOISE) then + return cWaveform.brown_noise_fn + elseif (form == cWaveform.FORM.VIOLET_NOISE) then + return cWaveform.violet_noise_fn + elseif (form == cWaveform.FORM.COPY) then + -- TODO + -- return xSampleBuffer.copy_fn_fn() + return cWaveform.sin_2pi_fn + end + + if not band_limited then + if (form == cWaveform.FORM.SIN) then + fn = cWaveform.sin_2pi_fn + elseif (form == cWaveform.FORM.SAW) then + fn = cWaveform.saw_fn + elseif (form == cWaveform.FORM.SQUARE) then + fn = cWaveform.square_fn + elseif (form == cWaveform.FORM.TRIANGLE) then + fn = cWaveform.triangle_fn + end + else + fn,mod = cWaveform.band_limited_fn_fn(form,cycle,shift,duty_onoff,duty,duty_var,duty_var_frq,range) + end + if (type(mod) == 'function') then + return function(x) + return fn(mod(x)) + end + else + return fn + end +end + + +--------------------------------------------------------------------------------------------------- +-- +-- @return function + +function cWaveform.cycle_phase_duty_mod (cycle,shift,duty_fiducal,duty_var,duty_var_frq) + + return function(x) + local xx = cWaveform.cycle_fmod(cWaveform.cycle_phase_mod(cycle,shift)(x)) + local xxx = duty_fiducal + duty_var*(1/2)*(-1*cWaveform.cos_fn(duty_var_frq*x)+1) + local y= cWaveform._duty_phase(xx,xxx) + return y + end +end + + +--------------------------------------------------------------------------------------------------- +-- @return boolean, true when x less or equal to 0 + +function cWaveform._torf(x) + return (x <= 0) and true or false +end + +--------------------------------------------------------------------------------------------------- +-- multi-random generator +-- rndm({{0,1,1},{100,1000}}) -> 0.2, 320, 0.3 ... + +function cWaveform.rndm(tbl) + local cnt = #tbl + local tp = math.random(cnt) + local x1,x2,idp = tbl[tp][1],tbl[tp][2],tbl[tp][3] + local y = (x2-x1)*math.random() + x1 + return cLib.round_with_precision(y,idp) +end + +--------------------------------------------------------------------------------------------------- +-- wv table + +function cWaveform.rtn_random_wave(wv) + local s = cWaveform.rndm({{1,1},{2,4},{1,4},{1,4},{1,8}}) + if (s == 1) then + wv.form = cWaveform.FORM.SIN + elseif (s == 2) then + wv.form = cWaveform.FORM.SAW + elseif (s == 3) then + wv.form = cWaveform.FORM.SQUARE + elseif (s == 4) then + wv.form = cWaveform.FORM.TRIANGLE + elseif (s == 5) then + wv.form = cWaveform.FORM.WHITE_NOISE + elseif (s == 6) then + wv.form = cWaveform.FORM.BROWN_NOISE + elseif (s == 7) then + wv.form = cWaveform.FORM.VIOLET_NOISE + end + return wv +end + +--------------------------------------------------------------------------------------------------- +-- @param wv (table) +-- @param a (number, the coefficient for long sample) +-- @return function,table or function (when modulated) + +function cWaveform.random_fn(wv,a,duty_off,range) + TRACE("cWaveform.random_fn(wv,a,duty_off,range)",wv,a,duty_off,range) + + if a == nil then + a = 1 + end + + --wv = cWaveform.rtn_rndm_mod(wv,a) + wv.cycle = cWaveform.rndm({{2,9},{1,8},{1,4}})*a + wv.shift = cWaveform.rndm({{0,0},{-1,1,2}}) + wv.duty = cWaveform.rndm({{50,50},{1,99},{50,52,1},{48,50,1},{10,90}}) + wv.duty_v = cWaveform.rndm({{0,0},{0,0},{-0.5,0.5,1},{-1,1,2},{0,10,2},{10,100}}) + wv.duty_v_f = cWaveform.rndm({{1,1},{-8,8},{-2000,2000}}) + wv.band_limited = cWaveform._torf(cWaveform.rndm({{1,1}})) + wv.duty_onoff = cWaveform._torf(cWaveform.rndm({{0,0},{0,0},{0,0},{0,0},{1,1}})) + + if (duty_off == true) then + wv.duty_onoff = false + end + wv = cWaveform.rtn_random_wave(wv) + + local _fn,_mod = cWaveform.wave_fn( + wv.form,wv.cycle,wv.shift,wv.duty_onoff, + wv.duty,wv.duty_v,wv.duty_v_f,wv.band_limited,range) + + if type(_mod) == 'function' then + return function(x) + return _fn(_mod(x)) + end + else + return _fn,wv + end + +end + + +--------------------------------------------------------------------------------------------------- +-- create random wave +-- @return function,table or function (when modulated) + +function cWaveform.random_copy_fn(range) + TRACE("cWaveform.random_copy_fn(range)",range) + + local wv = {} + wv.cycle = cWaveform.rndm({{1,4},{0.5,0.5},{0.5,0.5}}) + wv.shift = cWaveform.rndm({{0,0}}) + wv.duty = cWaveform.rndm({{50,50},{50,50},{50,52,1},{48,50,1},{10,90}}) + wv.duty_v = cWaveform.rndm({{0,0},{0,0},{0,0},{-0.5,0.5,1},{-1,1,2},{0,10,2},{10,100}}) + wv.duty_v_f = cWaveform.rndm({{1,1},{-8,8},{-2000,2000}}) + wv.band_limited = cWaveform._torf(cWaveform.rndm({{1,1}})) + wv.duty_onoff = cWaveform._torf(cWaveform.rndm({{0,0},{1,1},{1,1}})) + wv.form = cWaveform.FORM.COPY + + local _fn,_mod = cWaveform.wave_fn( + wv.form,wv.cycle,wv.shift,wv.duty_onoff, + wv.duty,wv.duty_v,wv.duty_v_f,wv.band_limited,range) + + if (type(_mod) == 'function') then + return function(x) + return _fn(_mod(x)) + end + else + return _fn,wv + end + +end + +--------------------------------------------------------------------------------------------------- +-- e.g. make_wave(cWaveform.table2fn({{0,1,0,-1,0},{}}) + +function cWaveform.table2fn(wave_tbl) + + local another = function(num,a,b) + if (num == a) then + return b + elseif (num == b) then + return a + else + return nil + end + end + + local count_tbl = {} + for i = 1,2 do + count_tbl[i] = table.count(wave_tbl[i]) + end + + return function (x,ch) + local _ch = ch + if (_ch == nil) then + _ch = 1 + end + if (count_tbl[_ch] == 0) then + _ch = another(_ch,1,2) + end + if (count_tbl[_ch] == 0) then + return 0 + end + local count = count_tbl[_ch] + -- wave_tbl[_ch][count] is reference data for the last point. + local xx = cWaveform.cycle_fmod(x*(count-1),count) + local x1 = math.floor(xx) +1 -- first index is 1 + local x2 = x1 +1 + if (x2 >= count) then + x2 = count -- Near the last point + end + local d = xx - (x1 -1) + return (wave_tbl[_ch][x1]) * (1-d) + (wave_tbl[_ch][x2]) * d + end +end + +--------------------------------------------------------------------------------------------------- +-- Create random waveform + +function cWaveform.random_wave(range) + TRACE("cWaveform.random_wave(range)",range) + + -- a: the coefficient for long sample + local a = cLib.round_value(range/167) + if a < 5 then a = 1 end + local fn = cWaveform.random_fn({},a,false,range) + for i= 1,5 do + fn = cWaveform.mix_fn_fn( + fn,cWaveform.random_fn({},a,false,range),math.random()) + end + + local wv_last = cWaveform.rtn_random_wave{} + local fn_last = cWaveform.wave_fn( + wv_last.form, a * cWaveform.rndm({{1,2},{1,4},{8,8},{16,16}}),0,false, + 50,0,1,true,range) + + return cWaveform.mix_fn_fn(fn,fn_last,math.random()*0.9+0.1) + +end + +--------------------------------------------------------------------------------------------------- +-- Helper functions +--------------------------------------------------------------------------------------------------- + +function cWaveform._duty_shape(duty,m) + -- d/50 integer*(1/m) + local d = cLib.round_value((duty/50)*m)/m*50 + if d< (1/m)*50 then + return (1/m)*50 + elseif d > 100 then + return 100 + end + return d +end + +--------------------------------------------------------------------------------------------------- + +function cWaveform._duty_phase(x,dty) + local d = dty/100 + local y = 0 + if (0 <= x and x < d) then + return (1/(2*d))*x + elseif (d <= x and x <= 1) then + return (1/(2*(1-d)))*(x-1)+1 + end + return 0 +end + +--------------------------------------------------------------------------------------------------- + +function cWaveform._max_even(num) + local n = math.floor(num) + if (math.fmod(n,2) == 1) then + return n -1 + else + return n + end +end + +--------------------------------------------------------------------------------------------------- +-- cycle_fmod (-0.2) -> 0.8 + +function cWaveform.cycle_fmod(x,m) + if not m then + m = 1 + end + return math.fmod((math.fmod(x,m)+m),m) +end + +--------------------------------------------------------------------------------------------------- + +function cWaveform.cycle_phase_mod(cycl,phs) + return function (x) + return cycl*x + phs + end +end + + +--------------------------------------------------------------------------------------------------- +-- BLITs (used for band limited waveforms) +--------------------------------------------------------------------------------------------------- + +function cWaveform._sinc_m_fn_fn(m) + if m == nil then m =1 end + return function(x) + local xx = math.sin(math.pi * x / m) + local y + if math.abs(xx) <= 1e-12 then + return math.cos(math.pi*x)/math.cos(math.pi*x/m) + else + return math.sin(math.pi * x) / (m * xx) + end + end +end + +--------------------------------------------------------------------------------------------------- +--[[ +function cWaveform.blit_m_p_tbl(m,p,range) + local sincm = cWaveform._sinc_m_fn_fn(m) + local tbl ={{},{}} + for i = 1,range+1 do + tbl[1][i] =(m/p)*sincm((i-1)*(m/p)) + end + return tbl +end +]] + +--------------------------------------------------------------------------------------------------- +--y(n+1) = y(n) + sin( PI * M / P * n) / (sin(PI / P * n) * P) - 1/P + +function cWaveform._blit_saw_tbl(p,shift,a,range) + + if (a == nil) then + a =1 + end + + local max_odd = function(num) + local n = math.floor(num) + if (math.fmod(n,2) == 1) then + return n + else + return n -1 + end + end + + p = p*a + range = range*a + + if (shift==nil) then + shift =0 + end + local sft = math.floor(p*cWaveform.cycle_fmod(shift+0.5)) + local m = max_odd(p/2) + if (m <=3) then + m =3 + end + + local sincm = cWaveform._sinc_m_fn_fn(m) + local tbl_pre ={} + local tbl ={{},{}} + local y,y_pre =0,(m/p/2) + local d = 1/p + local m_p = m/p + for i = 1,cLib.round_value(range+1+sft) do + y = (y_pre -(m_p)*sincm((i-1)*(m/p)) +d) + tbl_pre[i]=y *(1/m_p*0.58) + y_pre = y + end + for j = 1,cLib.round_value(range+1) do + tbl[1][j]= tbl_pre[j+sft] + end + return tbl +end + +--------------------------------------------------------------------------------------------------- + +function cWaveform._blit_square_tbl(p,shift,a,range) + if (a == nil) then + a = 1 + end + p= p*a + range = range*a + + if (shift==nil) then + shift =0 + end + local sft = math.floor(p*cWaveform.cycle_fmod(shift)) + local p = p/2 + local m = cWaveform._max_even(p/2) + if (m <= 2) then + m =2 + end + local sincm = cWaveform._sinc_m_fn_fn(m) + local tbl_pre ={} + local tbl ={{},{}} + local y,y_pre =0,-(m/p/2) + local m_p = m/p + for i = 1,cLib.round_value(range+1+sft) do + y = (y_pre +(m_p)*sincm((i-1)*(m/p)) ) + tbl_pre[i]=y *(1/m_p*0.58) + y_pre = y + end + for j = 1,cLib.round_value(range+1) do + tbl[1][j]= tbl_pre[j+sft] + end + return tbl +end + +--------------------------------------------------------------------------------------------------- + +function cWaveform._blit_triangle_tbl(p,shift,a,range) + if (a == nil) then + a =1 + end + p= p*a + range = range*a + + if (shift==nil) then + shift =0 + end + local sft = math.floor(p*cWaveform.cycle_fmod(shift+0.25)) + local pp = p/2 + local m = cWaveform._max_even(pp/2) + if (m <= 2) then + m =2 + end + local sincm = cWaveform._sinc_m_fn_fn(m) + local square = {} + local tbl_pre ={} + local tbl ={{},{}} + local m_pp = m/pp + local y,y_pre =0,-(m_pp/2) + local yy,yy_pre = 0,-(m_pp/2) + for i = 1,cLib.round_value(range+1+sft) do + y = (y_pre +(m_pp)*sincm((i-1)*(m/pp)) ) + square[i]=y + y_pre = y + end + for j = 1,cLib.round_value(range+1+sft) do + yy= yy_pre + square[j]/pp + tbl_pre[j] = yy*(1/(m_pp/2)*0.8) + yy_pre = yy + end + for k = 1,cLib.round_value(range+1) do + tbl[1][k]= tbl_pre[k+sft] + end + return tbl +end + +--------------------------------------------------------------------------------------------------- +-- @return function + +function cWaveform._blit_duty_fn_fn(form,p,duty,range) + TRACE("cWaveform._blit_duty_fn_fn(form,p,duty,range)",form,p,duty,range) + --print("duty "..duty) + local form_f = cWaveform._blit_square_tbl + if (form == cWaveform.FORM.SAW) then + form_f = cWaveform._blit_saw_tbl + elseif (form == cWaveform.FORM.SQUARE) then + form_f = cWaveform._blit_square_tbl + elseif (form == cWaveform.FORM.TRIANGLE) then + form_f = cWaveform._blit_triangle_tbl + end + + duty = cWaveform._duty_shape(duty,range) + local half = cWaveform._duty_shape(50,range) + local a1,a2 + a1 = (duty/50) ;a2 = 2 - a1 + local fn_1,fn_2,m_1,m_2 + fn_1 = cWaveform.table2fn(form_f(p,0,a1,range)) + fn_2 = cWaveform.table2fn(form_f(p,0,a2,range)) + return function(x) + local xx = math.fmod(x,(p/range))/(p/range) + local out + if (xx < 0.5) then + return fn_1(x,1) + elseif (xx >= 0.5) then + return fn_2(x,1) + else + return 0 + end + end +end + diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxdom.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxdom.lua new file mode 100644 index 00000000..bcb7c481 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxdom.lua @@ -0,0 +1,49 @@ +-- Optional parser that creates a flat DOM from parsing +--local SLAXML = require 'slaxml' +function SLAXML:dom(xml,opts) + if not opts then opts={} end + local rich = not opts.simple + local push, pop = table.insert, table.remove + local stack = {} + local doc = { type="document", name="#doc", kids={} } + local current = doc + local builder = SLAXML:parser{ + startElement = function(name,nsURI) + local el = { type="element", name=name, kids={}, el=rich and {} or nil, attr={}, nsURI=nsURI, parent=rich and current or nil } + if current==doc then + if doc.root then error(("Encountered element '%s' when the document already has a root '%s' element"):format(name,doc.root.name)) end + doc.root = el + end + push(current.kids,el) + if current.el then push(current.el,el) end + current = el + push(stack,el) + end, + attribute = function(name,value,nsURI) + if not current or current.type~="element" then error(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name,value)) end + local attr = {type='attribute',name=name,nsURI=nsURI,value=value,parent=rich and current or nil} + if rich then current.attr[name] = value end + push(current.attr,attr) + end, + closeElement = function(name) + if current.name~=name or current.type~="element" then error(("Received a close element notification for '%s' but was inside a '%s' %s"):format(name,current.name,current.type)) end + pop(stack) + current = stack[#stack] + end, + text = function(value) + if current.type~='document' then + if current.type~="element" then error(("Received a text notification '%s' but was inside a %s"):format(value,current.type)) end + push(current.kids,{type='text',name='#text',value=value,parent=rich and current or nil}) + end + end, + comment = function(value) + push(current.kids,{type='comment',name='#comment',value=value,parent=rich and current or nil}) + end, + pi = function(name,value) + push(current.kids,{type='pi',name=name,value=value,parent=rich and current or nil}) + end + } + builder:parse(xml,opts) + return doc +end +return SLAXML \ No newline at end of file diff --git a/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxml.lua b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxml.lua new file mode 100644 index 00000000..37d93986 --- /dev/null +++ b/Tools/com.renoise.Sononymph.xrnx/source/cLib/classes/support/slaxdom/slaxml.lua @@ -0,0 +1,257 @@ +--[=====================================================================[ +v0.7 Copyright © 2013-2014 Gavin Kistner ; MIT Licensed +See http://github.com/Phrogz/SLAXML for details. +--]=====================================================================] +local SLAXML = { + VERSION = "0.7", + _call = { + pi = function(target,content) + print (string.format("",target,content)) + end, + comment = function(content) + print (string.format("",content)) + end, + startElement = function(name,nsURI,nsPrefix) + io.write("<") + if nsPrefix then io.write(nsPrefix,":") end + io.write(name) + if nsURI then io.write(" (ns='",nsURI,"')") end + print (">") + end, + attribute = function(name,value,nsURI,nsPrefix) + io.write(' ') + if nsPrefix then io.write(nsPrefix,":") end + io.write(name,'=',string.format('%q',value)) + if nsURI then io.write(" (ns='",nsURI,"')") end + io.write("\n") + end, + text = function(text) + print (string.format(" text: %q",text)) + end, + closeElement = function(name,nsURI,nsPrefix) + print (string.format("",name)) + end, + } +} + +function SLAXML:parser(callbacks) + return { _call=callbacks or self._call, parse=SLAXML.parse } +end + +function SLAXML:parse(xml,options) + if not options then options = { stripWhitespace=false } end + + -- Cache references for maximum speed + local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat + local first, last, match1, match2, match3, pos2, nsURI + local unpack = unpack or table.unpack + local pos = 1 + local state = "text" + local textStart = 1 + local currentElement={} + local currentAttributes={} + local currentAttributeCt -- manually track length since the table is re-used + local nsStack = {} + local anyElement = false + + local utf8markers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} } + local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string + if decimal<128 then return char(decimal) end + local charbytes = {} + for bytes,vals in ipairs(utf8markers) do + if decimal<=vals[1] then + for b=bytes+1,2,-1 do + local mod = decimal%64 + decimal = (decimal-mod)/64 + charbytes[b] = char(128+mod) + end + charbytes[1] = char(vals[2]+decimal) + return concat(charbytes) + end + end + end + local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" } + local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8(tonumber('0'..s)) or orig end + local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end + + local function finishText() + if first>textStart and self._call.text then + local text = sub(xml,textStart,first-1) + if options.stripWhitespace then + text = gsub(text,'^%s+','') + text = gsub(text,'%s+$','') + if #text==0 then text=nil end + end + if text then self._call.text(unescape(text)) end + end + end + + local function findPI() + first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos ) + if first then + finishText() + if self._call.pi then self._call.pi(match1,match2) end + pos = last+1 + textStart = pos + return true + end + end + + local function findComment() + first, last, match1 = find( xml, '^', pos ) + if first then + finishText() + if self._call.comment then self._call.comment(match1) end + pos = last+1 + textStart = pos + return true + end + end + + local function nsForPrefix(prefix) + if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl + for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end + error(("Cannot find namespace for prefix %s"):format(prefix)) + end + + local function startElement() + anyElement = true + first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos ) + if first then + currentElement[2] = nil -- reset the nsURI, since this table is re-used + currentElement[3] = nil -- reset the nsPrefix, since this table is re-used + finishText() + pos = last+1 + first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos ) + if first then + currentElement[1] = match2 + currentElement[3] = match1 -- Save the prefix for later resolution + match1 = match2 + pos = last+1 + else + currentElement[1] = match1 + for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end + end + currentAttributeCt = 0 + push(nsStack,{}) + return true + end + end + + local function findAttribute() + first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos ) + if first then + pos2 = last+1 + first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands + if first then + pos = last+1 + match2 = unescape(match2) + else + first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands + if first then + pos = last+1 + match2 = unescape(match2) + end + end + end + if match1 and match2 then + local currentAttribute = {match1,match2} + local prefix,name = string.match(match1,'^([^:]+):([^:]+)$') + if prefix then + if prefix=='xmlns' then + nsStack[#nsStack][name] = match2 + else + currentAttribute[1] = name + currentAttribute[4] = prefix + end + else + if match1=='xmlns' then + nsStack[#nsStack]['!'] = match2 + currentElement[2] = match2 + end + end + currentAttributeCt = currentAttributeCt + 1 + currentAttributes[currentAttributeCt] = currentAttribute + return true + end + end + + local function findCDATA() + first, last, match1 = find( xml, '^', pos ) + if first then + finishText() + if self._call.text then self._call.text(match1) end + pos = last+1 + textStart = pos + return true + end + end + + local function closeElement() + first, last, match1 = find( xml, '^%s*(/?)>', pos ) + if first then + state = "text" + pos = last+1 + textStart = pos + + -- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed + if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end + if self._call.startElement then self._call.startElement(unpack(currentElement)) end + if self._call.attribute then + for i=1,currentAttributeCt do + if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end + self._call.attribute(unpack(currentAttributes[i])) + end + end + + if match1=="/" then + pop(nsStack) + if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end + end + return true + end + end + + local function findElementClose() + first, last, match1, match2 = find( xml, '^', pos ) + if first then + nsURI = nil + for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end + else + first, last, match2, match1 = find( xml, '^', pos ) + if first then nsURI = nsForPrefix(match2) end + end + if first then + finishText() + if self._call.closeElement then self._call.closeElement(match1,nsURI) end + pos = last+1 + textStart = pos + pop(nsStack) + return true + end + end + + while pos<#xml do + if state=="text" then + if not (findPI() or findComment() or findCDATA() or findElementClose()) then + if startElement() then + state = "attributes" + else + first, last = find( xml, '^[^<]+', pos ) + pos = (first and last or pos) + 1 + end + end + elseif state=="attributes" then + if not findAttribute() then + if not closeElement() then + error("Was in an element and couldn't find attributes or the close.") + end + end + end + end + + if not anyElement then error("Parsing did not discover any elements") end + if #nsStack > 0 then error("Parsing ended with unclosed elements") end +end + +return SLAXML From 04ee9b32e07a4a97a7784957514111e7d71696c0 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Sun, 6 Jul 2025 22:40:33 +0300 Subject: [PATCH 07/18] cLib has been improved on, therefore Sononymph doesn't need a submodule --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 246aec57..04b8367f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -130,6 +130,3 @@ [submodule "Tools/com.renoise.SelectionShaper.xrnx/source/vLib"] path = Tools/com.renoise.SelectionShaper.xrnx/source/vLib url = https://github.com/renoise/vLib.git -[submodule "Tools/com.renoise.Sononymph.xrnx/source/cLib"] - path = Tools/com.renoise.Sononymph.xrnx/source/cLib - url = https://github.com/renoise/cLib.git From d75878adde7524dde440a5cc3e9bc35402169cc4 Mon Sep 17 00:00:00 2001 From: esaruoho Date: Mon, 7 Jul 2025 03:52:27 +0300 Subject: [PATCH 08/18] 1.1.0 part3 --- Tools/com.renoise.Sononymph.xrnx/README.md | 4 ++-- Tools/com.renoise.Sononymph.xrnx/changelog.md | 10 +++------- Tools/com.renoise.Sononymph.xrnx/main.lua | 2 +- Tools/com.renoise.Sononymph.xrnx/manifest.xml | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/README.md b/Tools/com.renoise.Sononymph.xrnx/README.md index 20b35809..c0602ba8 100644 --- a/Tools/com.renoise.Sononymph.xrnx/README.md +++ b/Tools/com.renoise.Sononymph.xrnx/README.md @@ -1,4 +1,4 @@ -# Sononymph - Paketti Modifications v0.92 +# Sononymph - Paketti Modifications v1.1.0 ![Splash Image](docs/splash-large.png) @@ -126,7 +126,7 @@ When enabled, automatically imports samples as you browse in Sononym: - **Original Tool**: danoise - **Paketti Modifications**: Esa Ruoho (Lackluster): http://patreon.com/esaruoho -- **Version**: 0.92 - Added Sample Navigator selected slot loading functionality +- **Version**: 1.1.0 - Added Sample Navigator selected slot loading functionality --- diff --git a/Tools/com.renoise.Sononymph.xrnx/changelog.md b/Tools/com.renoise.Sononymph.xrnx/changelog.md index 66ec7ea5..fc0893b4 100644 --- a/Tools/com.renoise.Sononymph.xrnx/changelog.md +++ b/Tools/com.renoise.Sononymph.xrnx/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 0.92 +## 1.1.0 ### New Features - **Sample Navigator Enhancement**: Added "Load Selected Sample to Selected Slot" function specifically for Sample Navigator context @@ -13,7 +13,7 @@ - Improved error handling for sample slot selection validation - Better user feedback with specific status messages for slot loading operations -## 0.91 +## 1.0.5 ### Major Changes - Removed vLib and xLib dependencies - dialog now uses native Renoise ViewBuilder to show an almost identical dialog. (Cleaned up 101 unused library files, making the tool much lighter) @@ -22,10 +22,6 @@ - Made "Autostart" text bold to match other labels - Fixed startup crash caused by DocumentNode constructor error - Removed AppPrefs.lua from the codebase, as it's consolidated into main.lua. - -## 0.9 - -### Small tweaks and improvements by Esa Ruoho (Lackluster) - Added menu entries in 6 different contexts (Instrument Box, Sample Editor, Sample Navigator, Main Menu) - Auto-transfer can now create new instruments or sample slots instead of just overwriting - Added functions for loading samples from Sononym with or without prompts @@ -59,7 +55,7 @@ - Better JSON config validation and error handling - Detect button behavior improved for single vs multiple version scenarios -## 0.52 +## 1.0 - Add `changelog.md` - `cLib.require()`, use for avoiding circular dependencies diff --git a/Tools/com.renoise.Sononymph.xrnx/main.lua b/Tools/com.renoise.Sononymph.xrnx/main.lua index 2ba32f86..3c59f48a 100644 --- a/Tools/com.renoise.Sononymph.xrnx/main.lua +++ b/Tools/com.renoise.Sononymph.xrnx/main.lua @@ -76,7 +76,7 @@ require ('App') -- local variables & initialization --------------------------------------------------------------------------------------------------- local TOOL_NAME = "Sononymph" -local TOOL_VERSION = "0.92" +local TOOL_VERSION = "1.1.0" local prefs = AppPrefs() renoise.tool().preferences = prefs diff --git a/Tools/com.renoise.Sononymph.xrnx/manifest.xml b/Tools/com.renoise.Sononymph.xrnx/manifest.xml index 7cc95208..146e5093 100644 --- a/Tools/com.renoise.Sononymph.xrnx/manifest.xml +++ b/Tools/com.renoise.Sononymph.xrnx/manifest.xml @@ -2,7 +2,7 @@ Sononymph with Paketti Modifications com.renoise.Sononymph - 0.92 + 1.1.0 6 danoise & esaruoho false From 2c6f700a2ee57235dba3671718ae100afc2758df Mon Sep 17 00:00:00 2001 From: esaruoho Date: Mon, 7 Jul 2025 03:53:27 +0300 Subject: [PATCH 09/18] valid feedback from ylmrx on naming --- Tools/com.renoise.Sononymph.xrnx/manifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/manifest.xml b/Tools/com.renoise.Sononymph.xrnx/manifest.xml index 146e5093..8046e2f3 100644 --- a/Tools/com.renoise.Sononymph.xrnx/manifest.xml +++ b/Tools/com.renoise.Sononymph.xrnx/manifest.xml @@ -1,6 +1,6 @@ - Sononymph with Paketti Modifications + Sononymph com.renoise.Sononymph 1.1.0 6 From 6bdb80a5b298f5a65b48a511f7640151b11b0950 Mon Sep 17 00:00:00 2001 From: esaruoho Date: Mon, 7 Jul 2025 03:56:13 +0300 Subject: [PATCH 10/18] another manifest change --- Tools/com.renoise.Sononymph.xrnx/manifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/manifest.xml b/Tools/com.renoise.Sononymph.xrnx/manifest.xml index 8046e2f3..79dfa10e 100644 --- a/Tools/com.renoise.Sononymph.xrnx/manifest.xml +++ b/Tools/com.renoise.Sononymph.xrnx/manifest.xml @@ -7,8 +7,7 @@ danoise & esaruoho false Integration - This tool adds Sononym integration to Renoise. Launch from the Tools/Instrument Box/Sample Editor/Sample Navigator etc menus, midimapping or keybinding. -major fixes by Esa Ruoho + This tool adds Sononym integration to Renoise. Launch from the Tools/Instrument Box/Sample Editor/Sample Navigator etc menus, midimapping or keybinding. http://patreon.com/esaruoho From 12f0368a340acd9c4d753613e9bf6c719de73d76 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Mon, 7 Jul 2025 04:19:29 +0300 Subject: [PATCH 11/18] 1.1.0 -> 1.10 --- Tools/com.renoise.Sononymph.xrnx/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/README.md b/Tools/com.renoise.Sononymph.xrnx/README.md index c0602ba8..337dab8b 100644 --- a/Tools/com.renoise.Sononymph.xrnx/README.md +++ b/Tools/com.renoise.Sononymph.xrnx/README.md @@ -1,4 +1,4 @@ -# Sononymph - Paketti Modifications v1.1.0 +# Sononymph v1.10 ![Splash Image](docs/splash-large.png) @@ -126,7 +126,7 @@ When enabled, automatically imports samples as you browse in Sononym: - **Original Tool**: danoise - **Paketti Modifications**: Esa Ruoho (Lackluster): http://patreon.com/esaruoho -- **Version**: 1.1.0 - Added Sample Navigator selected slot loading functionality +- **Version**: 1.10 - Added Sample Navigator selected slot loading functionality --- From 255bd258c28bea3991b1f41b8164bf42cd6bdaf8 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Mon, 7 Jul 2025 04:19:50 +0300 Subject: [PATCH 12/18] 1.1.0->1.10 --- Tools/com.renoise.Sononymph.xrnx/changelog.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/changelog.md b/Tools/com.renoise.Sononymph.xrnx/changelog.md index fc0893b4..b0421531 100644 --- a/Tools/com.renoise.Sononymph.xrnx/changelog.md +++ b/Tools/com.renoise.Sononymph.xrnx/changelog.md @@ -1,6 +1,6 @@ # Changelog -## 1.1.0 +## 1.10 ### New Features - **Sample Navigator Enhancement**: Added "Load Selected Sample to Selected Slot" function specifically for Sample Navigator context @@ -13,7 +13,7 @@ - Improved error handling for sample slot selection validation - Better user feedback with specific status messages for slot loading operations -## 1.0.5 +## 1.05 ### Major Changes - Removed vLib and xLib dependencies - dialog now uses native Renoise ViewBuilder to show an almost identical dialog. (Cleaned up 101 unused library files, making the tool much lighter) @@ -70,4 +70,4 @@ ## 0.5 -- Standalone version \ No newline at end of file +- Standalone version From fc653909a87ad048eed0e7eb5505fdaca472f075 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Mon, 7 Jul 2025 04:20:09 +0300 Subject: [PATCH 13/18] tool export can't handle 1.1.0 - must be 1.10 --- Tools/com.renoise.Sononymph.xrnx/manifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/manifest.xml b/Tools/com.renoise.Sononymph.xrnx/manifest.xml index 79dfa10e..c18033f9 100644 --- a/Tools/com.renoise.Sononymph.xrnx/manifest.xml +++ b/Tools/com.renoise.Sononymph.xrnx/manifest.xml @@ -2,7 +2,7 @@ Sononymph com.renoise.Sononymph - 1.1.0 + 1.10 6 danoise & esaruoho false From 80d7c6ceece0124146e7fad613b33c33cf3556bc Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Mon, 7 Jul 2025 04:20:37 +0300 Subject: [PATCH 14/18] tool export can't handle 1.1.0 so 1.10 it is --- Tools/com.renoise.Sononymph.xrnx/main.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/main.lua b/Tools/com.renoise.Sononymph.xrnx/main.lua index 3c59f48a..a73f51e7 100644 --- a/Tools/com.renoise.Sononymph.xrnx/main.lua +++ b/Tools/com.renoise.Sononymph.xrnx/main.lua @@ -76,7 +76,7 @@ require ('App') -- local variables & initialization --------------------------------------------------------------------------------------------------- local TOOL_NAME = "Sononymph" -local TOOL_VERSION = "1.1.0" +local TOOL_VERSION = "1.10" local prefs = AppPrefs() renoise.tool().preferences = prefs From dfb78fffd116333766ed959d0ec94cafafe0a297 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Tue, 8 Jul 2025 02:01:51 +0300 Subject: [PATCH 15/18] one missing & added --- Tools/com.renoise.Sononymph.xrnx/App.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/App.lua b/Tools/com.renoise.Sononymph.xrnx/App.lua index 2419ba3d..ca2aff90 100644 --- a/Tools/com.renoise.Sononymph.xrnx/App.lua +++ b/Tools/com.renoise.Sononymph.xrnx/App.lua @@ -759,7 +759,7 @@ local tmp_path=cFilesystem.unixslashes(tmp_path) local path_to_exe = cFilesystem.unixslashes(self.prefs.path_to_exe.value) local cmd = string.format('"%s" %s',path_to_exe,cFilesystem.unixslashes(tmp_path)) print (cmd) - local code = os.execute(cmd) + local code = os.execute(cmd .. " &") return true end @@ -1589,4 +1589,4 @@ function flip_a_coin(file_path) return display_name, full_path end --- flip_a_coin(renoise.tool().preferences.path_to_config.value) -- Removed auto-call \ No newline at end of file +-- flip_a_coin(renoise.tool().preferences.path_to_config.value) -- Removed auto-call From 71a9025b783cd7e75da06ed54f24d4afdaa1ef83 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Tue, 8 Jul 2025 02:02:54 +0300 Subject: [PATCH 16/18] and another & --- Tools/com.renoise.Sononymph.xrnx/App.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/App.lua b/Tools/com.renoise.Sononymph.xrnx/App.lua index ca2aff90..bebee3bd 100644 --- a/Tools/com.renoise.Sononymph.xrnx/App.lua +++ b/Tools/com.renoise.Sononymph.xrnx/App.lua @@ -789,7 +789,7 @@ function App:do_browse() TRACE("Launching Sononym browse mode:", cmd) local success = pcall(function() - os.execute(cmd) + os.execute(cmd .. " &") end) if success then From cf8c09b5c5775643b494404b6d8b46e06457b4a0 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Tue, 8 Jul 2025 02:04:05 +0300 Subject: [PATCH 17/18] ampersand ampersand ampersand --- Tools/com.renoise.Sononymph.xrnx/App.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/App.lua b/Tools/com.renoise.Sononymph.xrnx/App.lua index bebee3bd..b61217d0 100644 --- a/Tools/com.renoise.Sononymph.xrnx/App.lua +++ b/Tools/com.renoise.Sononymph.xrnx/App.lua @@ -1534,7 +1534,7 @@ local os_name = os.platform() if os_name == "WINDOWS" then command = 'start "" "' .. directory_path .. '"' elseif os_name == "MACINTOSH" then command = 'open "' .. directory_path .. '"' else os_name = 'xdg-open "' .. directory_path .. '"' end - os.execute(command) + os.execute(command .. " &") end From f6ad7492df74016c669cba272e6fb9327f1aaa63 Mon Sep 17 00:00:00 2001 From: esaruoho Date: Tue, 14 Oct 2025 11:40:17 +0200 Subject: [PATCH 18/18] instrument naming update --- Tools/com.renoise.Sononymph.xrnx/App.lua | 44 +++++++++++----------- Tools/com.renoise.Sononymph.xrnx/AppUI.lua | 4 +- Tools/com.renoise.Sononymph.xrnx/main.lua | 3 ++ 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Tools/com.renoise.Sononymph.xrnx/App.lua b/Tools/com.renoise.Sononymph.xrnx/App.lua index b61217d0..53359d7e 100644 --- a/Tools/com.renoise.Sononymph.xrnx/App.lua +++ b/Tools/com.renoise.Sononymph.xrnx/App.lua @@ -495,15 +495,26 @@ function App:do_transfer() -- if any of these are true, instrument gets name of sample local created_instrument = false local instr_named_after_sample = false - - -if self.prefs.autotransfercreateslot.value then - renoise.song().selected_instrument:insert_sample_at(#renoise.song().selected_instrument.samples+1) - renoise.song().selected_sample_index = #renoise.song().selected_instrument.samples -elseif self.prefs.autotransfercreatenew.value then - renoise.song():insert_instrument_at(renoise.song().selected_instrument_index+1) - renoise.song().selected_instrument_index = renoise.song().selected_instrument_index + 1 -end + local created_new_instrument = false + + + -- Debug: Show current preference values + TRACE("=== AUTO-TRANSFER DEBUG ===") + TRACE("autotransfercreateslot.value =", self.prefs.autotransfercreateslot.value) + TRACE("autotransfercreatenew.value =", self.prefs.autotransfercreatenew.value) + + if self.prefs.autotransfercreateslot.value then + TRACE("Taking CREATE SLOT path...") + renoise.song().selected_instrument:insert_sample_at(#renoise.song().selected_instrument.samples+1) + renoise.song().selected_sample_index = #renoise.song().selected_instrument.samples + elseif self.prefs.autotransfercreatenew.value then + TRACE("Taking CREATE NEW INSTRUMENT path...") + renoise.song():insert_instrument_at(renoise.song().selected_instrument_index+1) + renoise.song().selected_instrument_index = renoise.song().selected_instrument_index + 1 + created_new_instrument = true + else + TRACE("Taking DEFAULT path (load to existing sample)...") + end local sample,instr = rns.selected_sample,rns.selected_instrument @@ -695,7 +706,7 @@ end local folder,filename,ext = cFilesystem.get_path_parts(self.selection_in_sononym.filename) sample.name = filename - if created_instrument or instr_named_after_sample then + if created_instrument or instr_named_after_sample or created_new_instrument then instr.name = filename end @@ -728,18 +739,7 @@ function App:do_search() return false,"There is no sample selected, doing nothing." end - -- show important notice the first time - if self.prefs.show_search_warning.value then - local choice = renoise.app():show_prompt("Important notice","" - .."Please make sure that Sononym is running before launching a search" - .."\n(NB: this message is only shown once!)" - ,{"Start Search","Cancel"}) - if (choice == "Cancel") then - return false - else - self.prefs.show_search_warning.value = false - end - end + -- Warning removed - user doesn't want to see it local success,err = App.check_path(self.prefs.path_to_exe.value) if not success then diff --git a/Tools/com.renoise.Sononymph.xrnx/AppUI.lua b/Tools/com.renoise.Sononymph.xrnx/AppUI.lua index 16b3e037..b8427069 100644 --- a/Tools/com.renoise.Sononymph.xrnx/AppUI.lua +++ b/Tools/com.renoise.Sononymph.xrnx/AppUI.lua @@ -568,8 +568,8 @@ function AppUI:update() local paths_are_valid = self.owner.paths_are_valid_observable.value local monitor_active = self.owner.monitor_active_observable.value ctrl.text = (paths_are_valid and monitor_active) - and "✔ Monitoring for changes..." - or "⚠ Invalid path: "..self.owner.invalid_path_observable.value + and "Monitoring for changes..." + or "Invalid path: "..self.owner.invalid_path_observable.value end diff --git a/Tools/com.renoise.Sononymph.xrnx/main.lua b/Tools/com.renoise.Sononymph.xrnx/main.lua index a73f51e7..e5d2e9ba 100644 --- a/Tools/com.renoise.Sononymph.xrnx/main.lua +++ b/Tools/com.renoise.Sononymph.xrnx/main.lua @@ -1,3 +1,6 @@ +local separator = package.config:sub(1,1) -- Gets \ for Windows, / for Unix + + --[[ This tool was originally created by danoise, and somewhat heavily modified by Esa Ruoho a.k.a. Lackluster.