hi, i’m wondering what the process of peak detection + normalizing could be optimized with?
i have the process slicer somehow set up, but i just can’t figure out how to optimize this better.
this function:
reads the selection of a slice, or if no selection and on slice, then that, or selection of a sample, or if no selection and on sample, then that.
it will normalize a selection of a slice, a complete slice, or a selection of a sample, or the whole sample.
but it just seems slow!
Normalizing 73119 frames (3.3 seconds at 22050Hz)
Peak amplitude: 0.766388 (-2.3 dB below full scale)
Will increase volume by 2.3 dB
Normalization complete:
Total time: 0.39 seconds (0.2M frames/sec)
Reading: 16.7%, Processing: 83.3%
Sample Selected is Sample Slot 1
Sample Frames Length is 1-23398260
Selection in Sample: 12451218-23398260
Normalizing: selection in sample
Normalizing 10947043 frames (496.5 seconds at 22050Hz)
Peak amplitude: 0.053680 (-25.4 dB below full scale)
Will increase volume by 25.4 dB
Normalization complete:
Total time: 26.76 seconds (0.4M frames/sec)
Reading: 36.1%, Processing: 63.9%
so, how do i optimize this? what do i do?
@martblek - i saw you locked your thread about a faster process slicer, but i’m not 100% where or how it was posted in full.
what’s the solution? @joule ? @Raul do any of you know what a process slicer is that could handle large files and peak detect + normalize?
function NormalizeSelectedSliceInSample()
local song = renoise.song()
local instrument = song.selected_instrument
local current_slice = song.selected_sample_index
local first_sample = instrument.samples[1]
local current_sample = song.selected_sample
-- Check if we have valid data
if not current_sample or not current_sample.sample_buffer.has_sample_data then
renoise.app():show_status("No sample available")
return
end
print(string.format("\nSample Selected is Sample Slot %d", song.selected_sample_index))
print(string.format("Sample Frames Length is 1-%d", current_sample.sample_buffer.number_of_frames))
-- Case 1: No slice markers - work on current sample
if #first_sample.slice_markers == 0 then
local buffer = current_sample.sample_buffer
local slice_start, slice_end
-- Check for selection in current sample
if buffer.selection_range[1] and buffer.selection_range[2] then
slice_start = buffer.selection_range[1]
slice_end = buffer.selection_range[2]
print(string.format("Selection in Sample: %d-%d", slice_start, slice_end))
print("Normalizing: selection in sample")
else
slice_start = 1
slice_end = buffer.number_of_frames
print("Normalizing: entire sample")
end
-- Create ProcessSlicer instance and dialog
local slicer = nil
local dialog = nil
local vb = nil
-- Define the process function
local function process_func()
local time_start = os.clock()
local time_reading = 0
local time_processing = 0
local total_frames = slice_end - slice_start + 1
print(string.format("\nNormalizing %d frames (%.1f seconds at %dHz)",
total_frames,
total_frames / buffer.sample_rate,
buffer.sample_rate))
-- First pass: Find peak
local peak = 0
local processed_frames = 0
local CHUNK_SIZE = 524288 -- 512KB worth of frames
-- Pre-allocate tables for better performance
local channel_peaks = {}
for channel = 1, buffer.number_of_channels do
channel_peaks[channel] = 0
end
buffer:prepare_sample_data_changes()
-- Process in blocks
for frame = slice_start, slice_end, CHUNK_SIZE do
local block_end = math.min(frame + CHUNK_SIZE - 1, slice_end)
local block_size = block_end - frame + 1
-- Read and process each channel
for channel = 1, buffer.number_of_channels do
local read_start = os.clock()
local channel_peak = 0
for i = 0, block_size - 1 do
local sample = math.abs(buffer:sample_data(channel, frame + i))
if sample > channel_peak then
channel_peak = sample
end
end
time_reading = time_reading + (os.clock() - read_start)
if channel_peak > channel_peaks[channel] then
channel_peaks[channel] = channel_peak
end
end
-- Update progress and yield
processed_frames = processed_frames + block_size
local progress = processed_frames / total_frames
if dialog and dialog.visible then
vb.views.progress_text.text = string.format("Finding peak... %.1f%%", progress * 100)
end
if slicer:was_cancelled() then
buffer:finalize_sample_data_changes()
return
end
coroutine.yield()
end
-- Find overall peak
for _, channel_peak in ipairs(channel_peaks) do
if channel_peak > peak then
peak = channel_peak
end
end
-- Check if sample is silent
if peak == 0 then
print("Sample is silent, no normalization needed")
buffer:finalize_sample_data_changes()
if dialog and dialog.visible then
dialog:close()
end
return
end
-- Calculate and display normalization info
local scale = 1.0 / peak
local db_increase = 20 * math.log10(scale)
print(string.format("\nPeak amplitude: %.6f (%.1f dB below full scale)", peak, -db_increase))
print(string.format("Will increase volume by %.1f dB", db_increase))
-- Reset progress for second pass
processed_frames = 0
-- Second pass: Apply normalization
for frame = slice_start, slice_end, CHUNK_SIZE do
local block_end = math.min(frame + CHUNK_SIZE - 1, slice_end)
local block_size = block_end - frame + 1
-- Process each channel
for channel = 1, buffer.number_of_channels do
local process_start = os.clock()
for i = 0, block_size - 1 do
local current_frame = frame + i
buffer:set_sample_data(channel, current_frame,
buffer:sample_data(channel, current_frame) * scale)
end
time_processing = time_processing + (os.clock() - process_start)
end
-- Update progress and yield
processed_frames = processed_frames + block_size
local progress = processed_frames / total_frames
if dialog and dialog.visible then
vb.views.progress_text.text = string.format("Normalizing... %.1f%%", progress * 100)
end
if slicer:was_cancelled() then
buffer:finalize_sample_data_changes()
return
end
coroutine.yield()
end
-- Finalize changes
buffer:finalize_sample_data_changes()
-- Calculate and display performance stats
local total_time = os.clock() - time_start
local frames_per_second = total_frames / total_time
print(string.format("\nNormalization complete:"))
print(string.format("Total time: %.2f seconds (%.1fM frames/sec)",
total_time, frames_per_second / 1000000))
print(string.format("Reading: %.1f%%, Processing: %.1f%%",
(time_reading/total_time) * 100,
((total_time - time_reading)/total_time) * 100))
-- Close dialog when done
if dialog and dialog.visible then
dialog:close()
end
if buffer.selection_range[1] and buffer.selection_range[2] then
renoise.app():show_status("Normalized selection in " .. current_sample.name)
else
renoise.app():show_status("Normalized " .. current_sample.name)
end
end
-- Create and start the ProcessSlicer
slicer = ProcessSlicer(process_func)
dialog, vb = slicer:create_dialog("Normalizing Sample")
slicer:start()
return
end
-- Case 2: Has slice markers
local buffer = first_sample.sample_buffer
local slice_start, slice_end
local slice_markers = first_sample.slice_markers
-- If we're on the first sample
if current_slice == 1 then
-- Check for selection in first sample
if buffer.selection_range[1] and buffer.selection_range[2] then
slice_start = buffer.selection_range[1]
slice_end = buffer.selection_range[2]
print(string.format("Selection in First Sample: %d-%d", slice_start, slice_end))
print("Normalizing: selection in first sample")
else
slice_start = 1
slice_end = buffer.number_of_frames
print("Normalizing: entire first sample")
end
else
-- Get slice boundaries
slice_start = current_slice > 1 and slice_markers[current_slice - 1] or 1
local slice_end_marker = slice_markers[current_slice] or buffer.number_of_frames
local slice_length = slice_end_marker - slice_start + 1
print(string.format("Selection is within Slice %d", current_slice))
print(string.format("Slice %d length is %d-%d (length: %d), within 1-%d of sample frames length",
current_slice, slice_start, slice_end_marker, slice_length, buffer.number_of_frames))
-- When in a slice, check the current_sample's selection range (slice view)
local current_buffer = current_sample.sample_buffer
-- Debug selection values
print(string.format("Current sample selection range: start=%s, end=%s",
tostring(current_buffer.selection_range[1]), tostring(current_buffer.selection_range[2])))
-- Check if there's a selection in the current slice view
if current_buffer.selection_range[1] and current_buffer.selection_range[2] then
local rel_sel_start = current_buffer.selection_range[1]
local rel_sel_end = current_buffer.selection_range[2]
-- Convert slice-relative selection to absolute position in sample
local abs_sel_start = slice_start + rel_sel_start - 1
local abs_sel_end = slice_start + rel_sel_end - 1
print(string.format("Selection %d-%d in slice view converts to %d-%d in sample",
rel_sel_start, rel_sel_end, abs_sel_start, abs_sel_end))
-- Use the converted absolute positions
slice_start = abs_sel_start
slice_end = abs_sel_end
print("Normalizing: selection in slice")
else
-- No selection in slice view - normalize whole slice
slice_end = slice_end_marker
print("Normalizing: entire slice (no selection in slice view)")
end
end
-- Ensure we don't exceed buffer bounds
slice_start = math.max(1, math.min(slice_start, buffer.number_of_frames))
slice_end = math.max(slice_start, math.min(slice_end, buffer.number_of_frames))
print(string.format("Final normalize range: %d-%d\n", slice_start, slice_end))
-- Create ProcessSlicer instance and dialog for sliced processing
local slicer = nil
local dialog = nil
local vb = nil
-- Define the process function for sliced processing
local function process_func()
local time_start = os.clock()
local time_reading = 0
local time_processing = 0
local total_frames = slice_end - slice_start + 1
print(string.format("\nNormalizing %d frames (%.1f seconds at %dHz)",
total_frames,
total_frames / buffer.sample_rate,
buffer.sample_rate))
-- First pass: Find peak
local peak = 0
local processed_frames = 0
local CHUNK_SIZE = 524288 -- 512KB worth of frames
-- Pre-allocate tables for better performance
local channel_peaks = {}
for channel = 1, buffer.number_of_channels do
channel_peaks[channel] = 0
end
buffer:prepare_sample_data_changes()
-- Process in blocks
for frame = slice_start, slice_end, CHUNK_SIZE do
local block_end = math.min(frame + CHUNK_SIZE - 1, slice_end)
local block_size = block_end - frame + 1
-- Read and process each channel
for channel = 1, buffer.number_of_channels do
local read_start = os.clock()
local channel_peak = 0
for i = 0, block_size - 1 do
local sample = math.abs(buffer:sample_data(channel, frame + i))
if sample > channel_peak then
channel_peak = sample
end
end
time_reading = time_reading + (os.clock() - read_start)
if channel_peak > channel_peaks[channel] then
channel_peaks[channel] = channel_peak
end
end
-- Update progress and yield
processed_frames = processed_frames + block_size
local progress = processed_frames / total_frames
if dialog and dialog.visible then
vb.views.progress_text.text = string.format("Finding peak... %.1f%%", progress * 100)
end
if slicer:was_cancelled() then
buffer:finalize_sample_data_changes()
return
end
coroutine.yield()
end
-- Find overall peak
for _, channel_peak in ipairs(channel_peaks) do
if channel_peak > peak then
peak = channel_peak
end
end
-- Check if sample is silent
if peak == 0 then
print("Sample is silent, no normalization needed")
buffer:finalize_sample_data_changes()
if dialog and dialog.visible then
dialog:close()
end
return
end
-- Calculate and display normalization info
local scale = 1.0 / peak
local db_increase = 20 * math.log10(scale)
print(string.format("\nPeak amplitude: %.6f (%.1f dB below full scale)", peak, -db_increase))
print(string.format("Will increase volume by %.1f dB", db_increase))
-- Reset progress for second pass
processed_frames = 0
-- Second pass: Apply normalization
for frame = slice_start, slice_end, CHUNK_SIZE do
local block_end = math.min(frame + CHUNK_SIZE - 1, slice_end)
local block_size = block_end - frame + 1
-- Process each channel
for channel = 1, buffer.number_of_channels do
local process_start = os.clock()
for i = 0, block_size - 1 do
local current_frame = frame + i
buffer:set_sample_data(channel, current_frame,
buffer:sample_data(channel, current_frame) * scale)
end
time_processing = time_processing + (os.clock() - process_start)
end
-- Update progress and yield
processed_frames = processed_frames + block_size
local progress = processed_frames / total_frames
if dialog and dialog.visible then
vb.views.progress_text.text = string.format("Normalizing... %.1f%%", progress * 100)
end
if slicer:was_cancelled() then
buffer:finalize_sample_data_changes()
return
end
coroutine.yield()
end
-- Finalize changes
buffer:finalize_sample_data_changes()
-- Calculate and display performance stats
local total_time = os.clock() - time_start
local frames_per_second = total_frames / total_time
print(string.format("\nNormalization complete:"))
print(string.format("Total time: %.2f seconds (%.1fM frames/sec)",
total_time, frames_per_second / 1000000))
print(string.format("Reading: %.1f%%, Processing: %.1f%%",
(time_reading/total_time) * 100,
((total_time - time_reading)/total_time) * 100))
-- Close dialog when done
if dialog and dialog.visible then
dialog:close()
end
-- Show appropriate status message
if current_slice == 1 then
if buffer.selection_range[1] and buffer.selection_range[2] then
renoise.app():show_status("Normalized selection in " .. current_sample.name)
else
renoise.app():show_status("Normalized entire sample")
end
else
if buffer.selection_range[1] and buffer.selection_range[2] then
renoise.app():show_status(string.format("Normalized selection in slice %d", current_slice))
else
renoise.app():show_status(string.format("Normalized slice %d", current_slice))
end
-- Refresh view for slices
song.selected_sample_index = song.selected_sample_index - 1
song.selected_sample_index = song.selected_sample_index + 1
end
end
-- Create and start the ProcessSlicer for sliced processing
slicer = ProcessSlicer(process_func)
dialog, vb = slicer:create_dialog("Normalizing Sample")
slicer:start()
end
and
--[[============================================================================
process_slicer.lua
============================================================================]]--
--[[
ProcessSlicer for Paketti - allows slicing up long-running operations into smaller chunks
to maintain UI responsiveness and show progress.
Main benefits:
- Shows current progress of operations
- Allows users to abort operations
- Prevents Renoise from thinking the script is frozen
- Maintains UI responsiveness during heavy operations
]]
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 = {...}
self.__process_thread = nil
self.__cancelled = false
end
--------------------------------------------------------------------------------
-- Returns true when the current process 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
--------------------------------------------------------------------------------
-- Cancel the process
function ProcessSlicer:cancel()
self.__cancelled = true
end
--------------------------------------------------------------------------------
-- Check if process was cancelled
function ProcessSlicer:was_cancelled()
return self.__cancelled
end
--------------------------------------------------------------------------------
-- Internal function called during idle to continue processing
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 it's 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()
-- Forward the error
error(error_message)
end
-- Stop when the process function completed
elseif (coroutine.status(self.__process_thread) == 'dead') then
self:stop()
end
end
-- Helper function to create a progress dialog
function ProcessSlicer:create_dialog(title)
local vb = renoise.ViewBuilder()
local dialog = nil
local DEFAULT_MARGIN = renoise.ViewBuilder.DEFAULT_CONTROL_MARGIN
local DEFAULT_SPACING = renoise.ViewBuilder.DEFAULT_CONTROL_SPACING
local dialog_content = vb:column {
margin = DEFAULT_MARGIN,
spacing = DEFAULT_SPACING,
vb:text {
id = "progress_text",
text = "Processing..."
},
vb:button {
id = "cancel_button",
text = "Cancel",
width = 80,
notifier = function()
self:cancel()
if dialog and dialog.visible then
dialog:close()
end
end
}
}
dialog = renoise.app():show_custom_dialog(
title or "Processing...", dialog_content)
return dialog, vb
end
what’s the right way?
@unless maybe knows something of this?