How to Sort Tables That Appear in Popup Menus?

I installedre.dread’sdBatchProcess which is a GUI frontend for sox that has presets for various audio processing effects.

New Tool (3.0): dBatchProcess

dBatchProcess.png

I’m not experienced with lua but I was able to figure out how to create custom presets, which are in a file called presets.lua, separate from the main.lua file.

The custom presets that I created work fine but they aren’t properly sortedin the popup menu. I’d like to keep similar commands, ie “Normalize -3db”, “Normalize -6db”, next to each other so that the list isn’t such a mess and the items are easier to locate.

Does anybody have an example on how to sort the popup list either alphabetically or by the order that the presets appear in the presets.lua file?

Here is the code for the 2 files…

main.lua

Click to view contents

– dBatchProcess

– …

– © 2014 re.dread


–[[ TODO:

]]–

require ‘process_slicer’

require ‘presets’

local TOOLTIPS = true

local tool_id = “dBatchProcess”

local tool_version = “0.1”

local dialog = nil

local vb = nil

local opt = renoise.Document.create(“ScriptingToolPreferences”) {

path = [[c:\Program Files (x86)\sox-14-4-1]],

cmd = [[sox in_file out_file silence 1 0.1 1% reverse silence 1 0.1 1% reverse]],

– 1 = instrument, 2 = samples

delete_source = 1,

}

renoise.tool().preferences = opt

– add menu entries

–[[

renoise.tool():add_menu_entry {

name = “Instrument Box: Batch process …”,

invoke = function() show_dialog(true) end

}

]]

renoise.tool():add_menu_entry {

name = “Sample Editor: Batch process …”,

invoke = function() show_dialog(true) end

}

renoise.tool():add_menu_entry {

name = “Sample Navigator: Batch process …”,

invoke = function() show_dialog(true) end

}

function quote(string)

return ((os.platform() == “WINDOWS”) and “”" … string … “”" or string)

end

function os.capture(cmd, raw)

local f = assert(io.popen(cmd, ‘r’))

local s = assert(f:read(’*a’))

f:close()

if raw then return s end

s = string.gsub(s, ‘^%s+’, ‘’)

s = string.gsub(s, ‘%s+$’, ‘’)

s = string.gsub(s, ‘[\n\r]+’, ’ ')

return s

end

local vol

function process_sample(sample, analyze)

if not sample then return end

local buffer = sample.sample_buffer

if not buffer.has_sample_data then return end

– save sample to temp location

local file_in = os.tmpname(“wav”)

buffer:save_as(file_in, “wav”)

– prepare output file

local file_out = os.tmpname(“wav”)

– substitute sample paths

local cmd = string.gsub(opt.cmd.value, “@in”, quote(file_in))

local cmd = string.gsub(cmd, “@out”, quote(file_out))

– kindof dirty, but seems to work

cmd = quote(opt.path.value) … cmd … " 2>&1 "

print(cmd)

– execute command

local cmd_output = os.capture(quote(cmd))

rprint(cmd_output)

local vol_find = string.find(cmd_output, “%d+%.%d+$”)

if vol_find then

local vol_tmp = tonumber(string.sub(cmd_output, vol_find))

if vol then

vol = (vol_tmp < vol) and vol_tmp or vol

else – first sample

vol = vol_tmp

end

end

if vol then

print(“Sample volume: -” … vol …“db”)

end

– reload sample

local output_file = io.exists(file_out)

if not output_file then

renoise.app():show_status(

analyze and “dBatchProcess: analyzing sample volumes” or

"dBatchProcess: WARNING: No output file produced, loading input file … ")

end

buffer:load_from(output_file and file_out or file_in)

end

function update_progress(sample_i, samples)

vb.views.process_all_btn.text = sample_i … " / " … samples

if sample_i == samples then

vb.views.process_all_btn.text = “Process all!”

end

end

function process_all_()

vol = nil

local instr = renoise.song().selected_instrument

local normalize = false

if opt.cmd.value == “@normalize” then

opt.cmd.value = “sox @in -n stat -v”

normalize = true

end

for sample_i, sample in ipairs(instr.samples) do

process_sample(sample, true)

update_progress(sample_i, #instr.samples)

coroutine:yield()

end

if normalize then

opt.cmd.value = (“sox -v %f @in @out”):format(vol - 0.01)

for sample_i, sample in ipairs(instr.samples) do

process_sample(sample)

update_progress(sample_i, #instr.samples)

coroutine:yield()

end

opt.cmd.value = “@normalize

end

end

local process

function process_all()

process = ProcessSlicer(process_all_, update_progress)

process:start()

–process_all_(update_progress)

end

function cancel()

if process and process:running() then

process:stop()

end

end


– ze gui


function show_dialog(instr)

if dialog and dialog.visible then

dialog:show()

return

end

vb = renoise.ViewBuilder()

local DEFAULT_DIALOG_MARGIN =

renoise.ViewBuilder.DEFAULT_DIALOG_MARGIN

local DEFAULT_CONTROL_SPACING =

renoise.ViewBuilder.DEFAULT_CONTROL_SPACING

local DEFAULT_BUTTON_HEIGHT =

renoise.ViewBuilder.DEFAULT_DIALOG_BUTTON_HEIGHT

– dialog content

local dialog_content = vb:column {

style = “body”,

margin = DEFAULT_DIALOG_MARGIN,

spacing = DEFAULT_CONTROL_SPACING,

uniform = true,

vb:horizontal_aligner {

–visible = false,

mode = “justify”,

vb:text { text = “Process samples”, font = “bold” },

},

vb:space { height = 5 },

vb:row {

style = “border”,

margin = DEFAULT_DIALOG_MARGIN,

spacing = DEFAULT_CONTROL_SPACING,

vb:column {

vb:text { text = "PATH " },

vb:text { text = "CMD " },

vb:text { text = "Presets: " },

},

vb:column {

spacing = DEFAULT_CONTROL_SPACING,

vb:horizontal_aligner {

mode = “justify”,

vb:textfield {

id = “path_txt”,

bind = opt.path,

width = 280,

},

vb:button {

id = “select_path”,

text = “*”,

notifier = function()

local new_path = renoise.app():prompt_for_path(“Executable path …”)

if new_path and new_path ~= “” then

opt.path.value = new_path

end

end,

},

},

vb:textfield {

id = “cmd_txt”,

bind = opt.cmd,

width = 300,

},

vb:popup {

items = table.keys(presets),

width = 300,

notifier = function(i)

vb.views.desc_txt.text = table.values(presets)[i].desc

opt.cmd.value = table.values(presets)[i].cmd

end

},

vb:multiline_textfield {

id = “desc_txt”,

width = 300,

height = 55,

text = “”

},

},

},

–vb:space { height = 5 },

vb:horizontal_aligner {

visible = false,

vb:checkbox {

bind = opt.delete_source_sample

},

vb:text { text = " delete source sample(s)" },

},

–vb:space { height = 10 },

vb:horizontal_aligner {

mode = “distribute”,

spacing = DEFAULT_CONTROL_SPACING,

margin = 5,

vb:button {

id = “process_btn”,

text = “Process sample!”,

notifier = function()

process_sample(renoise.song().selected_sample)

end

},

vb:button {

id = “process_all_btn”,

text = “Process all!”,

notifier = process_all,

},

vb:button {

text = “Cancel!”,

notifier = cancel,

},

},

}

– key_handler

local function key_handler(dialog, key)

– ignore held keys

if (key.repeated) then

return

end

if (key.name == “esc”) then

dialog:close()

end

end

– show

dialog = renoise.app():show_custom_dialog(

tool_id … " " … tool_version, dialog_content, key_handler)

end

presets.lua

Click to view contents

– presets:

presets = {

[“SoX: Normalize (Relative)”] =

{

cmd = “@normalize”,

desc = [[Normalize all samples relative to the loudest sample.]],

},

[“SoX: Trim silence (start/end)”] =

{

cmd = “sox @in @out silence 1 0.1 0.1% reverse silence 1 0.1 0.1% reverse”,

desc = [[Trim silence from start and end of the sample.

0.1% is the threshold value.]],

},

[“SoX: Change sample speed”] =

{

cmd = “sox @in @out speed 1200c”,

desc = [[Speeds up the sample with an amount of cents, setting it to 1200 is like shifting the octave.]],

},

[“SoX: Contrast”] =

{

cmd = “sox @in @out contrast 50”,

desc = [[Comparable with compression, this effect modifies an audio signal to make it sound louder. enhancement-amount controls the amount of the enhancement and is a number in the range 0−100. Note that enhancement-amount = 0 still gives a significant contrast enhancement.]],

},

[“SoX: Change sampling rate”] =

{

cmd = “sox @in -r 22k @out”,

desc = [[Change the sampleing rate. Here 22k]],

},

[“SoX: Echo (Short metallic)”] =

{

cmd = [[sox @in @out echo 0.8 0.88 6 0.4]],

desc = [[echo gain-in gain-out

Add echoing to the audio. Echoes are reflected sound and can occur naturally amongst mountains (and sometimes large buildings) when talking or shouting; digital echo effects emulate this behaviour and are often used to help fill out the sound of a single instrument or vocal. The time difference between the original signal and the reflection is the ‘delay’ (time), and the loudness of the reflected signal is the ‘decay’. Multiple echoes can have different delays and decays.]],

},

[“SoX: Fade in/out”] =

{

cmd = [[sox @in @out fade 0.01 0 0.01]],

desc = [[fade [type] fade-in-length [stop-time [fade-out-length]]

Apply a fade effect to the beginning, end, or both of the audio.

An optional type can be specified to select the shape of the fade curve: q for quarter of a sine wave, h for half a sine wave, t for linear (‘triangular’) slope, l for logarithmic, and p for inverted parabola. The default is logarithmic.

A fade-in starts from the first sample and ramps the signal level from 0 to full volume over fade-in-length seconds. Specify 0 seconds if no fade-in is wanted.

For fade-outs, the audio will be truncated at stop-time and the signal level will be ramped from full volume down to 0 starting at fade-out-length seconds before the stop-time. If fade-out-length is not specified, it defaults to the same value as fade-in-length. No fade-out is performed if stop-time is not specified. If the file length can be determined from the input file header and length-changing effects are not in effect, then 0 may be specified for stop-time to indicate the usual case of a fade-out that ends at the end of the input audio stream.

All times can be specified in either periods of time or sample counts. To specify time periods use the format hh:mm:ss.frac format. To specify using sample counts, specify the number of samples and append the letter ‘s’ to the sample count (for example ‘8000s’).]],

},

[“SoX: Normalize”] =

{

cmd = [[sox @in @out norm 0.01]],

desc = [[norm [dB-level]

Normalise the audio. norm is just an alias for gain −n; see the gain effect for details.]],

},

[“SoX: Reverb”] =

{

cmd = [[sox @in @out pad 0 3 reverb]],

desc = [[reverb [−w|−−wet-only] [reverberance (50%) [HF-damping (50%)

[room-scale (100%) [stereo-depth (100%)

[pre-delay (0ms) [wet-gain (0dB)]]]]]]

Add reverberation to the audio using the ‘freeverb’ algorithm. A reverberation effect is sometimes desirable for concert halls that are too small or contain so many people that the hall’s natural reverberance is diminished. Applying a small amount of stereo reverb to a (dry) mono signal will usually make it sound more natural. See [3] for a detailed description of reverberation.]],

},

}

Thanks.

Have a try to see if this trick works in a listbox as well? I haven’t tried it but i would guess it works. (I’m also guessing you are creative enough to implement it with a sorting algorithm)

https://forum.renoise.com/t/sorting-my-sub-menu/39044

EDIT: woops… this answer might have been too advanced. But this is how I would try modifying the dBatchProcess script.

Thanks for the response.

I tried your suggestion. Using two line feeds didn’t work. Putting just one, at the end of each line, seemed to work but it didn’t help sort the menu alphabetically. It just sorted the titles in a different arbitrary order.

The presets file uses a table structure known as associative array or dictionary and those have no guaranteed order and are accessed by their key name. To have those values sorted you need to change to an index based array. You could change the presets file to a new format or rewrite the table once when the tool loads the file. You also have to adjust the places in the code then where the table is accessed.

Thanks for the advice. I think it’s a bit beyond my experience level though.

Here you go… modified presets file, added table sorting to main file and also fixed a small bug with description not showing for preset when dialog is loaded for first time and popup list wasn’t changed yet. Haven’t tested this with an actual Sox installation but should be fine.

Thank you so much. It’s was very generous of you take the time to do this. It works just fine with SoX installed.

I hate to bring this up but I noticed another issue that is bugging me now…

The CMD box shows the last command that was used but the preset and description always shows the first preset. This means that if you want to use the first preset, after using another one, you have to pick another preset and then go back to first one in order to reset it. For some reason the command for the last preset used is written to the preferences.xml file as a reference.

Should be fixed now, see other post.

Once again, thanks for taking the time to make these modifications for me.

It works great and really helps to streamline the workflow with this tool.