key_assign.lua

— Section: Dependencies
local reaper = reaper
local KeyAssignDisplay = require(“graphics.key_assign_display”)
local filter_params = require(“parameters.filter_params”)
local FilterDisplay = require(“graphics.filter_display”)
local EnvelopeDisplay = require(“graphics.envelope_display”)

— Section: Dummy Data Loading
local dummy_samples = nil
local dummy_data_path = “dummy_samples”
local success, result = pcall(require, dummy_data_path)
if success then
dummy_samples = result
print(“KeyAssign: Loaded dummy_samples from “, dummy_data_path)
else
print(“KeyAssign: Failed to load dummy_samples: “, result)
dummy_samples = { Sample = {}, [“Key Assign”] = {“keymap01”} }
end

— Validate dummy_samples
if not dummy_samples.Sample or type(dummy_samples.Sample) ~= “table” then
print(“KeyAssign: Warning: dummy_samples.Sample invalid, using empty table”)
dummy_samples.Sample = {}
end
if not dummy_samples[“Key Assign”] or type(dummy_samples[“Key Assign”]) ~= “table” then
print(“KeyAssign: Warning: dummy_samples[‘Key Assign’] invalid, using default”)
dummy_samples[“Key Assign”] = {“keymap01”}
end

— Section: KeyAssign Class
local KeyAssign = {}
KeyAssign.__index = KeyAssign

function KeyAssign:new(data_manager, sysex)
print(“KeyAssign:new: Initializing”)
local sample_list = dummy_samples.Sample or data_manager.parameters.settings_list.Samples or {“No Samples”}
local initial_programs = {}
for _, keymap_name in ipairs(dummy_samples[“Key Assign”]) do
initial_programs[keymap_name] = {
key_assignments = {},
sample_list = self:deep_copy(sample_list),
sample_parameters = {},
sample_display_names = {}
}
end
if not next(initial_programs) then
initial_programs.keymap01 = {
key_assignments = {},
sample_list = self:deep_copy(sample_list),
sample_parameters = {},
sample_display_names = {}
}
end

local self = setmetatable({
data_manager = data_manager,
sysex = sysex or { send_sysex = function() end, send_midi_note = function() end },
programs = initial_programs,
current_program = dummy_samples[“Key Assign”][1] or “keymap01”,
key_assignments = {},
sample_list = sample_list,
sample_parameters = {},
sample_display_names = {},
selected_note = nil,
selected_layer = 1,
selected_sample = nil,
current_tab = 0,
tabs = { “Mapping”, “Crossfades”, “Parameters” },
auto_assign_open = false,
auto_assign_samples = {},
auto_assign_mapping = 0,
auto_assign_start_note = 60,
auto_assign_range_low = 60,
auto_assign_range_high = 84,
auto_assign_crossfade = false,
auto_assign_random_pitch = false,
auto_assign_random_direction = false,
auto_assign_layers = false,
auto_assign_round_robin = false,
mapping_options = { “Chromatic”, “Velocity”, “Random” },
filter_options = filter_params.filter_types or { “LowPass1”, “HighPass”, “Bypass” },
output_options = { “Output 1”, “Output 2”, “Output 3”, “Output 4” },
direction_options = { “Forward”, “Reverse”, “Alternate” },
display = KeyAssignDisplay:new(data_manager, sysex),
copied_note = nil,
copied_pane = nil,
pending_refresh = false,
selected_program = nil,
selected_bank = nil,
assign_popup_open = false,
assign_popup_samples = {},
save_keymap_popup_open = false,
save_keymap_name = “NewKeymap”,
filter_display = FilterDisplay:new(),
envelope_display = EnvelopeDisplay:new(),
undo_stack = {},
redo_stack = {},
max_undo_steps = 50
}, KeyAssign)
self:initialize_program(self.current_program)
return self
end

function KeyAssign:deep_copy(tbl, cache)
cache = cache or {}
if type(tbl) ~= “table” then return tbl end
if cache[tbl] then return cache[tbl] end
local copy = {}
cache[tbl] = copy
for k, v in pairs(tbl) do
copy[k] = self:deep_copy(v, cache)
end
return copy
end

function KeyAssign:push_undo_state(action_description)
local snapshot = {
key_assignments = self:deep_copy(self.key_assignments),
sample_list = self:deep_copy(self.sample_list),
sample_parameters = self:deep_copy(self.sample_parameters),
sample_display_names = self:deep_copy(self.sample_display_names),
current_program = self.current_program,
selected_note = self.selected_note,
selected_layer = self.selected_layer,
copied_note = self:deep_copy(self.copied_note),
description = action_description
}
table.insert(self.undo_stack, snapshot)
if #self.undo_stack > self.max_undo_steps then
table.remove(self.undo_stack, 1)
end
self.redo_stack = {}
self.data_manager.status_message = “Action: ” .. action_description
print(“KeyAssign:push_undo_state: Pushed snapshot for “, action_description)
end

function KeyAssign:undo()
if #self.undo_stack == 0 then
self.data_manager.status_message = “Nothing to undo”
print(“KeyAssign:undo: No actions to undo”)
return
end
local snapshot = table.remove(self.undo_stack)
table.insert(self.redo_stack, {
key_assignments = self:deep_copy(self.key_assignments),
sample_list = self:deep_copy(self.sample_list),
sample_parameters = self:deep_copy(self.sample_parameters),
sample_display_names = self:deep_copy(self.sample_display_names),
current_program = self.current_program,
selected_note = self.selected_note,
selected_layer = self.selected_layer,
copied_note = self:deep_copy(self.copied_note),
description = snapshot.description
})
self.key_assignments = self:deep_copy(snapshot.key_assignments)
self.sample_list = self:deep_copy(snapshot.sample_list)
self.sample_parameters = self:deep_copy(snapshot.sample_parameters)
self.sample_display_names = self:deep_copy(snapshot.sample_display_names)
self.current_program = snapshot.current_program
self.selected_note = snapshot.selected_note
self.selected_layer = snapshot.selected_layer
self.copied_note = self:deep_copy(snapshot.copied_note)
self.programs[self.current_program] = {
key_assignments = self:deep_copy(snapshot.key_assignments),
sample_list = self:deep_copy(snapshot.sample_list),
sample_parameters = self:deep_copy(snapshot.sample_parameters),
sample_display_names = self:deep_copy(snapshot.sample_display_names)
}
self.data_manager.key_assign = self.data_manager.key_assign or {}
self.data_manager.key_assign.sample_list = self.sample_list
self.data_manager.refresh_ui = true
self.pending_refresh = true
self.data_manager.status_message = “Undo: ” .. snapshot.description
print(“KeyAssign:undo: Restored state for “, snapshot.description)
end

function KeyAssign:redo()
if #self.redo_stack == 0 then
self.data_manager.status_message = “Nothing to redo”
print(“KeyAssign:redo: No actions to redo”)
return
end
local snapshot = table.remove(self.redo_stack)
table.insert(self.undo_stack, {
key_assignments = self:deep_copy(self.key_assignments),
sample_list = self:deep_copy(self.sample_list),
sample_parameters = self:deep_copy(self.sample_parameters),
sample_display_names = self:deep_copy(self.sample_display_names),
current_program = self.current_program,
selected_note = self.selected_note,
selected_layer = self.selected_layer,
copied_note = self:deep_copy(self.copied_note),
description = snapshot.description
})
self.key_assignments = self:deep_copy(snapshot.key_assignments)
self.sample_list = self:deep_copy(snapshot.sample_list)
self.sample_parameters = self:deep_copy(snapshot.sample_parameters)
self.sample_display_names = self:deep_copy(snapshot.sample_display_names)
self.current_program = snapshot.current_program
self.selected_note = snapshot.selected_note
self.selected_layer = snapshot.selected_layer
self.copied_note = self:deep_copy(snapshot.copied_note)
self.programs[self.current_program] = {
key_assignments = self:deep_copy(snapshot.key_assignments),
sample_list = self:deep_copy(snapshot.sample_list),
sample_parameters = self:deep_copy(snapshot.sample_parameters),
sample_display_names = self:deep_copy(snapshot.sample_display_names)
}
self.data_manager.key_assign = self.data_manager.key_assign or {}
self.data_manager.key_assign.sample_list = self.sample_list
self.data_manager.refresh_ui = true
self.pending_refresh = true
self.data_manager.status_message = “Redo: ” .. snapshot.description
print(“KeyAssign:redo: Restored state for “, snapshot.description)
end

function KeyAssign:initialize_program(program_name)
if not self.programs[program_name] then
self.programs[program_name] = {
key_assignments = {},
sample_list = self:deep_copy(self.sample_list),
sample_parameters = {},
sample_display_names = {}
}
for note = 0, 127 do
self.programs[program_name].key_assignments[note] = {
samples = {},
velocity_ranges = {},
note_range = {},
crossfades = { key = 0, velocity = 0 },
direction = “Forward”,
round_robin = false
}
end
local normalized_list, display_names = self:normalize_sample_names(self.sample_list)
self.programs[program_name].sample_list = normalized_list
self.programs[program_name].sample_display_names = display_names
for _, sample in ipairs(normalized_list) do
self:initialize_sample_parameters(program_name, sample)
end
end
self:load_program(program_name)
end

function KeyAssign:initialize_sample_parameters(program_name, sample)
local program = self.programs[program_name]
if not program.sample_parameters[sample] then
local envelope = self.data_manager:get_sample_envelopes(sample) or {
Amplitude = { Attack = 0, Decay = 50, Sustain = 70, Release = 20, Depth = 0 },
Filter = { Attack = 0, Decay = 50, Sustain = 70, Release = 20, Depth = 0 },
Pitch = { Attack = 0, Decay = 50, Sustain = 70, Release = 20, Depth = 0 }
}
local filter_state = self.data_manager:get_sample_filter(sample) or {
type = “LowPass1”,
parameters = { [“Cutoff Frequency”] = 500, Resonance = 0 }
}
program.sample_parameters[sample] = {
envelope = envelope,
filter = {
filter_type = filter_state.type,
cutoff_frequency = filter_state.parameters[“Cutoff Frequency”],
resonance = filter_state.parameters.Resonance
},
loop = {
loop_mode = 0,
wave_start_address = { L = 0, R = 0 },
wave_length = { L = 0, R = 0 },
wave_end_address = 0,
loop_start_address = { L = 0, R = 0 },
loop_length = { L = 0, R = 0 },
loop_end_address = 0,
start_address_velocity_sensitivity = 0,
loop_tempo = 8000
},
playback = {
sample_level = 0,
pan = 0,
velocity_low_limit = 0,
velocity_offset = 0,
velocity_range_high = 127,
velocity_range_low = 0,
level_key_scaling_break_point_1 = 0,
level_key_scaling_break_point_2 = 127,
level_key_scaling_level_1 = 0,
level_key_scaling_level_2 = 0,
velocity_sensitivity = 0,
sample_portamento_type = 0,
sample_portamento_rate = 1,
sample_portamento_time = 1
},
tuning = {
pitch_bend_type = 0,
pitch_bend_range = 0,
original_key = { L = 60, R = 60 },
fine_tune = { L = 0, R = 0 },
coarse_tune = 0,
detune = 0,
dephase = 0,
expand_width = 0,
random_pitch = 0,
fixed_pitch_on = 0
},
lfo = {
lfo_wave = 0,
lfo_speed = 0,
lfo_delay_time = 0,
lfo_sync_on = 0,
lfo_pitch_mod_phase_invert_on = 0,
lfo_cutoff_mod_phase_invert_on = 0
},
eq = {
eq_frequency = 4,
eq_gain = 52,
eq_width = 10,
eq_type = 0
},
mod_matrix = {
sources = { “LFO”, “Velocity”, “Mod Wheel”, “Aftertouch” },
destinations = { “Pitch”, “Filter Cutoff”, “Amplitude”, “Pan” },
assignments = {}
}
}
self.data_manager:set_sample_envelopes(sample, envelope)
self.data_manager:set_sample_filter(sample, filter_state)
— Send initial sysex for all parameters
self.sysex:send_sysex({ p1 = “filter_type”, p2 = sample, value = filter_state.type })
self.sysex:send_sysex({ p1 = “filter_param”, p2 = “cutoff_frequency”, sample = sample, value = filter_state.parameters[“Cutoff Frequency”] })
self.sysex:send_sysex({ p1 = “filter_param”, p2 = “resonance”, sample = sample, value = filter_state.parameters.Resonance })
for _, env_type in ipairs({ “Amplitude”, “Filter”, “Pitch” }) do
for _, param in ipairs({ “Attack”, “Decay”, “Sustain”, “Release”, “Depth” }) do
self.sysex:send_sysex({ p1 = “envelope”, p2 = env_type .. “_” .. param, sample = sample, value = envelope[env_type][param] })
end
end
for _, param in ipairs({ “loop_mode”, “wave_end_address”, “loop_end_address”, “start_address_velocity_sensitivity”, “loop_tempo” }) do
self.sysex:send_sysex({ p1 = “loop”, p2 = param, sample = sample, value = program.sample_parameters[sample].loop[param] })
end
for _, channel in ipairs({ “L”, “R” }) do
self.sysex:send_sysex({ p1 = “loop”, p2 = “wave_start_address_” .. channel, sample = sample, value = program.sample_parameters[sample].loop.wave_start_address[channel] })
self.sysex:send_sysex({ p1 = “loop”, p2 = “wave_length_” .. channel, sample = sample, value = program.sample_parameters[sample].loop.wave_length[channel] })
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_start_address_” .. channel, sample = sample, value = program.sample_parameters[sample].loop.loop_start_address[channel] })
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_length_” .. channel, sample = sample, value = program.sample_parameters[sample].loop.loop_length[channel] })
end
for _, param in ipairs({ “sample_level”, “pan”, “velocity_low_limit”, “velocity_offset”, “velocity_range_high”, “velocity_range_low”, “level_key_scaling_break_point_1”, “level_key_scaling_break_point_2”, “level_key_scaling_level_1”, “level_key_scaling_level_2”, “velocity_sensitivity”, “sample_portamento_type”, “sample_portamento_rate”, “sample_portamento_time” }) do
self.sysex:send_sysex({ p1 = “playback”, p2 = param, sample = sample, value = program.sample_parameters[sample].playback[param] })
end
for _, param in ipairs({ “pitch_bend_type”, “pitch_bend_range”, “coarse_tune”, “detune”, “dephase”, “expand_width”, “random_pitch”, “fixed_pitch_on” }) do
self.sysex:send_sysex({ p1 = “tuning”, p2 = param, sample = sample, value = program.sample_parameters[sample].tuning[param] })
end
for _, channel in ipairs({ “L”, “R” }) do
self.sysex:send_sysex({ p1 = “tuning”, p2 = “original_key_” .. channel, sample = sample, value = program.sample_parameters[sample].tuning.original_key[channel] })
self.sysex:send_sysex({ p1 = “tuning”, p2 = “fine_tune_” .. channel, sample = sample, value = program.sample_parameters[sample].tuning.fine_tune[channel] })
end
for _, param in ipairs({ “lfo_wave”, “lfo_speed”, “lfo_delay_time”, “lfo_sync_on”, “lfo_pitch_mod_phase_invert_on”, “lfo_cutoff_mod_phase_invert_on” }) do
self.sysex:send_sysex({ p1 = “lfo”, p2 = param, sample = sample, value = program.sample_parameters[sample].lfo[param] })
end
for _, param in ipairs({ “eq_frequency”, “eq_gain”, “eq_width”, “eq_type” }) do
self.sysex:send_sysex({ p1 = “eq”, p2 = param, sample = sample, value = program.sample_parameters[sample].eq[param] })
end
end
if self.current_program == program_name then
self.sample_parameters[sample] = self:deep_copy(program.sample_parameters[sample])
end
end

function KeyAssign:save_sample_parameters(sample)
if not self.current_program then return end
self:push_undo_state(“Save sample parameters for ” .. (sample or “all samples”))
local program = self.programs[self.current_program]
if sample then
program.sample_parameters[sample] = self:deep_copy(self.sample_parameters[sample])
self.data_manager:set_sample_parameters(sample, self.sample_parameters[sample])
else
for sample_id, params in pairs(self.sample_parameters) do
program.sample_parameters[sample_id] = self:deep_copy(params)
self.data_manager:set_sample_parameters(sample_id, params)
end
end
self:save_program_state()
end

function KeyAssign:save_program_state()
if not self.current_program then
self.current_program = dummy_samples[“Key Assign”][1] or “keymap01”
self:initialize_program(self.current_program)
end
for note = 0, 127 do
local data = self.key_assignments[note] or { samples = {}, velocity_ranges = {}, note_range = {}, crossfades = { key = 0, velocity = 0 }, direction = “Forward”, round_robin = false }
local num_layers = #data.samples
for i = #data.velocity_ranges + 1, num_layers do
data.velocity_ranges[i] = { min = 0, max = 127 }
end
for i = #data.note_range + 1, num_layers do
data.note_range[i] = { low = note, high = note }
end
while #data.velocity_ranges > num_layers do
table.remove(data.velocity_ranges)
end
while #data.note_range > num_layers do
table.remove(data.note_range)
end
self.key_assignments[note] = data
— Send sysex for note-specific parameters
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_key”, note = note, value = data.crossfades.key })
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_velocity”, note = note, value = data.crossfades.velocity })
self.sysex:send_sysex({ p1 = “note”, p2 = “direction”, note = note, value = data.direction })
self.sysex:send_sysex({ p1 = “note”, p2 = “round_robin”, note = note, value = data.round_robin and 1 or 0 })
for i, sample in ipairs(data.samples) do
self.sysex:send_sysex({ p1 = “note”, p2 = “sample”, note = note, layer = i, value = sample })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_min”, note = note, layer = i, value = data.velocity_ranges[i].min })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_max”, note = note, layer = i, value = data.velocity_ranges[i].max })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_low”, note = note, layer = i, value = data.note_range[i].low })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_high”, note = note, layer = i, value = data.note_range[i].high })
end
end
self.programs[self.current_program] = {
key_assignments = self:deep_copy(self.key_assignments),
sample_list = self:deep_copy(self.sample_list),
sample_parameters = self:deep_copy(self.sample_parameters),
sample_display_names = self:deep_copy(self.sample_display_names)
}
end

function KeyAssign:load_program(program_name)
if not program_name then return false end
self:push_undo_state(“Load program ” .. program_name)
local program_data = self.programs[program_name]
if not program_data then
self:initialize_program(program_name)
program_data = self.programs[program_name]
end
self.key_assignments = self:deep_copy(program_data.key_assignments or {})
self.sample_list = self:deep_copy(program_data.sample_list or self.sample_list)
self.sample_parameters = self:deep_copy(program_data.sample_parameters or {})
self.sample_display_names = self:deep_copy(program_data.sample_display_names or {})
for note = 0, 127 do
if not self.key_assignments[note] then
self.key_assignments[note] = { samples = {}, velocity_ranges = {}, note_range = {}, crossfades = { key = 0, velocity = 0 }, direction = “Forward”, round_robin = false }
end
— Send sysex for note-specific parameters
local data = self.key_assignments[note]
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_key”, note = note, value = data.crossfades.key })
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_velocity”, note = note, value = data.crossfades.velocity })
self.sysex:send_sysex({ p1 = “note”, p2 = “direction”, note = note, value = data.direction })
self.sysex:send_sysex({ p1 = “note”, p2 = “round_robin”, note = note, value = data.round_robin and 1 or 0 })
for i, sample in ipairs(data.samples) do
self.sysex:send_sysex({ p1 = “note”, p2 = “sample”, note = note, layer = i, value = sample })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_min”, note = note, layer = i, value = data.velocity_ranges[i].min })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_max”, note = note, layer = i, value = data.velocity_ranges[i].max })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_low”, note = note, layer = i, value = data.note_range[i].low })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_high”, note = note, layer = i, value = data.note_range[i].high })
end
end
self.current_program = program_name
self.data_manager.key_assign = self.data_manager.key_assign or {}
self.data_manager.key_assign.sample_list = self.sample_list
for _, sample in ipairs(self.sample_list) do
self:initialize_sample_parameters(program_name, sample)
end
self.pending_refresh = true
self.data_manager.status_message = “Loaded program: ” .. program_name
return true
end

function KeyAssign:get_program_samples(program_name)
local samples = {}
local program = self.programs[program_name]
if program then
for _, sample_name in ipairs(program.sample_list) do
table.insert(samples, { program = program_name, sample = sample_name })
end
end
return samples
end

function KeyAssign:get_samples_for_assignment(program_name, bank_name)
local samples = {}
for _, sample_name in ipairs(self.sample_list) do
table.insert(samples, { program = program_name or “default”, sample = sample_name })
end
return samples
end

function KeyAssign:draw(ctx, sample_list, filter_settings)
if not reaper.ImGui_ValidatePtr(ctx, ‘ImGui_Context*’) then
print(“KeyAssign:draw: Invalid ImGui context”)
reaper.ImGui_Text(ctx, “Error: Invalid ImGui context”)
return
end
local success, err = pcall(function()
filter_settings = filter_settings or {}
if sample_list and #sample_list > 0 then
self:push_undo_state(“Update sample list”)
local normalized_list, display_names = self:normalize_sample_names(sample_list)
self.sample_list = normalized_list
self.sample_display_names = display_names
for note, data in pairs(self.key_assignments) do
local i = 1
while i <= #data.samples do if not self:table_contains(self.sample_list, data.samples[i]) then table.remove(data.samples, i) table.remove(data.velocity_ranges, i) table.remove(data.note_range, i) else i = i + 1 end end if #data.samples == 0 then self.key_assignments[note] = { samples = {}, velocity_ranges = {}, note_range = {}, crossfades = { key = 0, velocity = 0 }, direction = "Forward", round_robin = false } end end for _, sample in ipairs(self.sample_list) do self:initialize_sample_parameters(self.current_program, sample) filter_settings[sample] = self.sample_parameters[sample].filter end self:save_sample_parameters() self:save_program_state() end if reaper.ImGui_BeginChild(ctx, "KeyAssignUI", 0, 0, true) then if self.display then self.display:draw(ctx, self) end reaper.ImGui_Separator(ctx) local program_names = {} for prog_name in pairs(self.programs) do table.insert(program_names, prog_name) end table.sort(program_names) if #program_names == 0 then table.insert(program_names, "No Keymaps") end local preview = self.current_program or program_names[1] reaper.ImGui_SetNextItemWidth(ctx, 200) if reaper.ImGui_BeginCombo(ctx, "Select Keymap##KeymapCombo", preview) then for _, prog_name in ipairs(program_names) do if reaper.ImGui_Selectable(ctx, prog_name, self.current_program == prog_name) then if prog_name ~= "No Keymaps" then self.current_program = prog_name self:load_program(prog_name) self.pending_refresh = true end end end reaper.ImGui_EndCombo(ctx) end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Save Keymap") then self.save_keymap_popup_open = true self.save_keymap_name = "NewKeymap" reaper.ImGui_OpenPopup(ctx, "SaveKeymapPopup") end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Delete Keymap") and self.current_program then self:push_undo_state("Delete keymap " .. self.current_program) self.programs[self.current_program] = nil self.current_program = next(self.programs) or dummy_samples["Key Assign"][1] self:initialize_program(self.current_program) self.pending_refresh = true self.data_manager.status_message = "Deleted keymap" end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Assign Samples") then self.assign_popup_open = true self.assign_popup_samples = {} reaper.ImGui_OpenPopup(ctx, "AssignSamplesPopup") end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Clear Zone") then self:push_undo_state("Clear zone") self.key_assignments = {} for note = 0, 127 do self.key_assignments[note] = { samples = {}, velocity_ranges = {}, note_range = {}, crossfades = { key = 0, velocity = 0 }, direction = "Forward", round_robin = false } end self.sample_list = dummy_samples.Sample or {"No Samples"} self.sample_parameters = {} self.sample_display_names = {} self:initialize_program(self.current_program) self:save_program_state() self.pending_refresh = true self.data_manager.status_message = "Cleared all key assignments" end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Undo") then self:undo() end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Redo") then self:redo() end reaper.ImGui_Text(ctx, self.data_manager.status_message or "") if reaper.ImGui_BeginPopupModal(ctx, "SaveKeymapPopup", nil, reaper.ImGui_WindowFlags_AlwaysAutoResize()) then reaper.ImGui_Text(ctx, "Enter Keymap Name:") reaper.ImGui_SetNextItemWidth(ctx, 200) local changed, new_name = reaper.ImGui_InputText(ctx, "##KeymapName", self.save_keymap_name) if changed then self.save_keymap_name = new_name end if reaper.ImGui_Button(ctx, "OK") then if self.save_keymap_name and self.save_keymap_name ~= "" then self:push_undo_state("Save keymap as " .. self.save_keymap_name) self.current_program = self.save_keymap_name self:save_program_state() self.pending_refresh = true self.data_manager.status_message = "Saved keymap: " .. self.save_keymap_name end self.save_keymap_popup_open = false reaper.ImGui_CloseCurrentPopup(ctx) end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Cancel") then self.save_keymap_popup_open = false reaper.ImGui_CloseCurrentPopup(ctx) end reaper.ImGui_EndPopup(ctx) end if reaper.ImGui_BeginPopupModal(ctx, "AssignSamplesPopup", nil, reaper.ImGui_WindowFlags_AlwaysAutoResize()) then reaper.ImGui_Text(ctx, "Assign Samples") reaper.ImGui_Text(ctx, "Select Samples:") if reaper.ImGui_BeginChild(ctx, "AssignSampleList", 200, 100, true) then for _, sample_data in ipairs(self:get_samples_for_assignment(nil, nil)) do local sample = sample_data.sample local display_name = self.sample_display_names[sample] or sample local changed, selected = reaper.ImGui_Checkbox(ctx, display_name .. "##Assign_" .. sample, self.assign_popup_samples[sample] or false) if changed then self.assign_popup_samples[sample] = selected end end reaper.ImGui_EndChild(ctx) end if reaper.ImGui_Button(ctx, "OK") then local selected_samples = {} for sample, selected in pairs(self.assign_popup_samples) do if selected then table.insert(selected_samples, sample) end end if #selected_samples > 0 then
self:push_undo_state(“Assign samples”)
self.sample_list = selected_samples
local normalized_list, display_names = self:normalize_sample_names(selected_samples)
self.sample_list = normalized_list
self.sample_display_names = display_names
for _, sample in ipairs(self.sample_list) do
self:initialize_sample_parameters(self.current_program, sample)
end
self:save_sample_parameters()
self:save_program_state()
self.data_manager.status_message = “Assigned samples”
end
self.assign_popup_open = false
reaper.ImGui_CloseCurrentPopup(ctx)
end
reaper.ImGui_SameLine(ctx)
if reaper.ImGui_Button(ctx, “Cancel”) then
self.assign_popup_open = false
reaper.ImGui_CloseCurrentPopup(ctx)
end
reaper.ImGui_EndPopup(ctx)
end
reaper.ImGui_Separator(ctx)
if reaper.ImGui_BeginTabBar(ctx, “KeyAssignTabs”) then
for i, tab in ipairs(self.tabs) do
if reaper.ImGui_BeginTabItem(ctx, tab) then
self.current_tab = i – 1
if tab == “Mapping” then
self:draw_mapping(ctx, filter_settings)
elseif tab == “Crossfades” then
self:draw_crossfades(ctx)
elseif tab == “Parameters” then
self:draw_parameters(ctx, filter_settings)
end
reaper.ImGui_EndTabItem(ctx)
end
end
reaper.ImGui_EndTabBar(ctx)
end
reaper.ImGui_EndChild(ctx)
end
end)
if not success then
print(“KeyAssign:draw: Error: “, err)
reaper.ImGui_Text(ctx, “Error: Failed to render UI (” .. tostring(err) .. “)”)
end
if self.pending_refresh then
self.data_manager.refresh_ui = true
self.pending_refresh = false
end
end

function KeyAssign:draw_mapping(ctx, filter_settings)
local success, err = pcall(function()
reaper.ImGui_Text(ctx, “Layer Management”)
if reaper.ImGui_Button(ctx, “Auto-Assign”) then
self.auto_assign_open = true
for _, sample in ipairs(self.sample_list) do
self.auto_assign_samples[sample] = false
end
reaper.ImGui_OpenPopup(ctx, “Auto-Assign Samples”)
end
reaper.ImGui_Separator(ctx)
for note = 0, 127 do
local data = self.key_assignments[note]
if data and #data.samples > 0 then
if reaper.ImGui_TreeNode(ctx, “Note ” .. note) then
for i, sample in ipairs(data.samples) do
local display_name = self.sample_display_names[sample] or sample
local label = “Layer ” .. i .. “: ” .. display_name
local is_selected = self.selected_note == note and self.selected_layer == i
if reaper.ImGui_Selectable(ctx, label, is_selected) then
self.selected_note = note
self.selected_layer = i
self.data_manager.refresh_ui = true
end
if reaper.ImGui_BeginPopupContextItem(ctx, “LayerContext_” .. note .. “_” .. i) then
if reaper.ImGui_Selectable(ctx, “Delete Layer”) then
table.remove(data.samples, i)
table.remove(data.velocity_ranges, i)
table.remove(data.note_range, i)
self.data_manager:save_program_state()
self.pending_refresh = true
end
reaper.ImGui_EndPopup(ctx)
end
local filter = self.sample_parameters[sample] and self.sample_parameters[sample].filter
reaper.ImGui_Text(ctx, ” Filter: ” .. (filter and filter.filter_type or “nil”))
end
if reaper.ImGui_BeginPopupContextItem(ctx, “NoteContext_” .. note) then
if reaper.ImGui_BeginMenu(ctx, “Add Layer”) then
for _, sample in ipairs(self.sample_list) do
local display_name = self.sample_display_names[sample] or sample
if reaper.ImGui_Selectable(ctx, display_name) then
table.insert(data.samples, sample)
table.insert(data.velocity_ranges, { min = 0, max = 127 })
table.insert(data.note_range, { low = note, high = note })
self:initialize_sample_parameters(self.current_program, sample)
filter_settings[sample] = self.sample_parameters[sample].filter
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
end
reaper.ImGui_EndMenu(ctx)
end
reaper.ImGui_EndPopup(ctx)
end
reaper.ImGui_TreePop(ctx)
end
end
end
if reaper.ImGui_BeginPopupContextWindow(ctx, “MappingContext”) then
if reaper.ImGui_BeginMenu(ctx, “Add Note”) then
reaper.ImGui_Text(ctx, “Select Sample:”)
for _, sample in ipairs(self.sample_list) do
local display_name = self.sample_display_names[sample] or sample
if reaper.ImGui_Selectable(ctx, display_name) then
local note = 60
self.key_assignments[note] = {
samples = { sample },
velocity_ranges = { { min = 0, max = 127 } },
note_range = { { low = note, high = note } },
crossfades = { key = 0, velocity = 0 },
direction = “Forward”,
round_robin = false
}
self:initialize_sample_parameters(self.current_program, sample)
filter_settings[sample] = self.sample_parameters[sample].filter
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
end
reaper.ImGui_EndMenu(ctx)
end
reaper.ImGui_EndPopup(ctx)
end
if self.selected_note and self.selected_layer then
local note_data = self.key_assignments[self.selected_note]
if note_data and note_data.samples[self.selected_layer] then
reaper.ImGui_Separator(ctx)
local display_name = self.sample_display_names[note_data.samples[self.selected_layer]] or note_data.samples[self.selected_layer]
reaper.ImGui_Text(ctx, “Layer Parameters for Note ” .. self.selected_note .. “, Layer ” .. self.selected_layer .. “: ” .. display_name)
local vel_range = note_data.velocity_ranges[self.selected_layer] or { min = 0, max = 127 }
local changed, new_min = reaper.ImGui_SliderInt(ctx, “Velocity Min##” .. self.selected_note .. “_” .. self.selected_layer, vel_range.min, 0, 127)
if changed then
note_data.velocity_ranges[self.selected_layer].min = new_min
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_min”, note = self.selected_note, layer = self.selected_layer, value = new_min })
self.pending_refresh = true
self:save_program_state()
end
changed, new_max = reaper.ImGui_SliderInt(ctx, “Velocity Max##” .. self.selected_note .. “_” .. self.selected_layer, vel_range.max, 0, 127)
if changed then
note_data.velocity_ranges[self.selected_layer].max = new_max
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_max”, note = self.selected_note, layer = self.selected_layer, value = new_max })
self.pending_refresh = true
self:save_program_state()
end
local note_range = note_data.note_range and note_data.note_range[self.selected_layer] or { low = self.selected_note, high = self.selected_note }
changed, new_low = reaper.ImGui_SliderInt(ctx, “Note Range Low##” .. self.selected_note .. “_” .. self.selected_layer, note_range.low, 0, 127)
if changed then
note_data.note_range[self.selected_layer].low = new_low
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_low”, note = self.selected_note, layer = self.selected_layer, value = new_low })
self.pending_refresh = true
self:save_program_state()
end
changed, new_high = reaper.ImGui_SliderInt(ctx, “Note Range High##” .. self.selected_note .. “_” .. self.selected_layer, note_range.high, 0, 127)
if changed then
note_data.note_range[self.selected_layer].high = new_high
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_high”, note = self.selected_note, layer = self.selected_layer, value = new_high })
self.pending_refresh = true
self:save_program_state()
end
local current_direction = note_data.direction or “Forward”
if reaper.ImGui_BeginCombo(ctx, “Direction##” .. self.selected_note .. “_” .. self.selected_layer, current_direction) then
for _, direction in ipairs(self.direction_options) do
if reaper.ImGui_Selectable(ctx, direction, current_direction == direction) then
note_data.direction = direction
self.sysex:send_sysex({ p1 = “note”, p2 = “direction”, note = self.selected_note, value = direction })
self.pending_refresh = true
self:save_program_state()
end
end
reaper.ImGui_EndCombo(ctx)
end
local round_robin = note_data.round_robin or false
changed, new_round_robin = reaper.ImGui_Checkbox(ctx, “Round Robin##” .. self.selected_note .. “_” .. self.selected_layer, round_robin)
if changed then
note_data.round_robin = new_round_robin
self.sysex:send_sysex({ p1 = “note”, p2 = “round_robin”, note = self.selected_note, value = new_round_robin and 1 or 0 })
self.pending_refresh = true
self:save_program_state()
end
end
end
if reaper.ImGui_BeginPopupModal(ctx, “Auto-Assign Samples”, nil, reaper.ImGui_WindowFlags_AlwaysAutoResize()) then
reaper.ImGui_Text(ctx, “Select Samples:”)
if reaper.ImGui_BeginChild(ctx, “SampleList”, 200, 100, true) then
for _, sample in ipairs(self.sample_list) do
local display_name = self.sample_display_names[sample] or sample
local changed, selected = reaper.ImGui_Checkbox(ctx, display_name .. “##AutoAssign_” .. sample, self.auto_assign_samples[sample] or false)
if changed then
self.auto_assign_samples[sample] = selected
end
end
reaper.ImGui_EndChild(ctx)
end
if reaper.ImGui_BeginCombo(ctx, “Mapping Type”, self.mapping_options[self.auto_assign_mapping + 1]) then
for i, mapping in ipairs(self.mapping_options) do
if reaper.ImGui_Selectable(ctx, mapping, self.auto_assign_mapping == i – 1) then
self.auto_assign_mapping = i – 1
end
end
reaper.ImGui_EndCombo(ctx)
end
local changed, new_start = reaper.ImGui_SliderInt(ctx, “Start Note”, self.auto_assign_start_note, 0, 127)
if changed then
self.auto_assign_start_note = new_start
end
changed, new_low = reaper.ImGui_SliderInt(ctx, “Range Low”, self.auto_assign_range_low, 0, 127)
if changed then
self.auto_assign_range_low = new_low
end
changed, new_high = reaper.ImGui_SliderInt(ctx, “Range High”, self.auto_assign_range_high, 0, 127)
if changed then
self.auto_assign_range_high = new_high
end
changed, new_crossfade = reaper.ImGui_Checkbox(ctx, “Crossfade”, self.auto_assign_crossfade)
if changed then
self.auto_assign_crossfade = new_crossfade
end
changed, new_random_pitch = reaper.ImGui_Checkbox(ctx, “Random Pitch”, self.auto_assign_random_pitch)
if changed then
self.auto_assign_random_pitch = new_random_pitch
end
changed, new_random_direction = reaper.ImGui_Checkbox(ctx, “Random Direction”, self.auto_assign_random_direction)
if changed then
self.auto_assign_random_direction = new_random_direction
end
changed, new_layers = reaper.ImGui_Checkbox(ctx, “Layers”, self.auto_assign_layers)
if changed then
self.auto_assign_layers = new_layers
end
changed, new_round_robin = reaper.ImGui_Checkbox(ctx, “Round Robin”, self.auto_assign_round_robin)
if changed then
self.auto_assign_round_robin = new_round_robin
end
if reaper.ImGui_Button(ctx, “OK”) then
local selected_samples = {}
for sample, selected in pairs(self.auto_assign_samples) do
if selected then
table.insert(selected_samples, sample)
end
end
if #selected_samples > 0 then
self:auto_assign_samples(selected_samples)
self.data_manager.key_assign.sample_list = selected_samples
for _, sample in ipairs(selected_samples) do
self:initialize_sample_parameters(self.current_program, sample)
filter_settings[sample] = self.sample_parameters[sample].filter
self:save_sample_parameters(sample)
end
self:save_program_state()
end
self.auto_assign_open = false
reaper.ImGui_CloseCurrentPopup(ctx)
end
reaper.ImGui_SameLine(ctx)
if reaper.ImGui_Button(ctx, “Cancel”) then
self.auto_assign_open = false
reaper.ImGui_CloseCurrentPopup(ctx)
end
reaper.ImGui_EndPopup(ctx)
end
end)
if not success then
print(“KeyAssign:draw_mapping: Error: “, err)
reaper.ImGui_Text(ctx, “Error: Failed to render mapping (” .. tostring(err) .. “)”)
end
end

function KeyAssign:draw_crossfades(ctx)
local success, err = pcall(function()
if not self.selected_note then
reaper.ImGui_Text(ctx, “Select a note to adjust crossfades”)
return
end
reaper.ImGui_Text(ctx, “Crossfade Settings for Note ” .. self.selected_note)
local data = self.key_assignments[self.selected_note]
if data then
local key_crossfade = data.crossfades.key or 0
local vel_crossfade = data.crossfades.velocity or 0
local changed, new_key = reaper.ImGui_SliderInt(ctx, “Key Crossfade##” .. self.selected_note, key_crossfade, 0, 127)
if changed then
data.crossfades.key = new_key
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_key”, note = self.selected_note, value = new_key })
self.pending_refresh = true
self:save_program_state()
end
changed, new_vel = reaper.ImGui_SliderInt(ctx, “Velocity Crossfade##” .. self.selected_note, vel_crossfade, 0, 127)
if changed then
data.crossfades.velocity = new_vel
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_velocity”, note = self.selected_note, value = new_vel })
self.pending_refresh = true
self:save_program_state()
end
end
end)
if not success then
print(“KeyAssign:draw_crossfades: Error: “, err)
reaper.ImGui_Text(ctx, “Error: Failed to render crossfades (” .. tostring(err) .. “)”)
end
end

function KeyAssign:draw_parameters(ctx, filter_settings)
local success, err = pcall(function()
print(“KeyAssign:draw_parameters: Entered for note=”, self.selected_note, ” layer=”, self.selected_layer)
if not self.selected_note or not self.selected_layer then
reaper.ImGui_Text(ctx, “Select a note and layer to edit parameters”)
print(“KeyAssign:draw_parameters: No note or layer selected”)
return
end
local note_data = self.key_assignments[self.selected_note]
if not note_data or not note_data.samples[self.selected_layer] then
print(“KeyAssign:draw_parameters: No sample for note=”, self.selected_note, ” layer=”, self.selected_layer)
reaper.ImGui_Text(ctx, “No sample assigned”)
return
end
local sample = note_data.samples[self.selected_layer]
local display_name = self.sample_display_names[sample] or sample
reaper.ImGui_Text(ctx, “Parameters for Note ” .. self.selected_note .. “, Layer ” .. self.selected_layer .. “: ” .. display_name)
local params = self.sample_parameters[sample]
if not params then
self:initialize_sample_parameters(self.current_program, sample)
params = self.sample_parameters[sample]
print(“KeyAssign:draw_parameters: Initialized parameters for “, sample)
end

if reaper.ImGui_BeginTabBar(ctx, “ParameterTabs_” .. self.selected_note .. “_” .. self.selected_layer) then
— Filter Tab
if reaper.ImGui_BeginTabItem(ctx, “Filter”) then
local filter = params.filter or { filter_type = “LowPass1”, cutoff_frequency = 500, resonance = 0 }
reaper.ImGui_SetNextItemWidth(ctx, 150)
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
if reaper.ImGui_BeginCombo(ctx, “Filter Type##” .. unique_id, filter.filter_type) then
for _, f_type in ipairs(self.filter_options) do
if reaper.ImGui_Selectable(ctx, f_type, f_type == filter.filter_type) then
local new_filter = {
filter_type = f_type,
cutoff_frequency = filter.cutoff_frequency or 500,
resonance = filter.resonance or 0
}
params.filter = new_filter
self.sample_parameters[sample].filter = new_filter
self.data_manager:set_sample_filter(sample, {
type = f_type,
parameters = { [“Cutoff Frequency”] = new_filter.cutoff_frequency, Resonance = new_filter.resonance }
})
filter_settings[sample] = new_filter
self.sysex:send_sysex({ p1 = “filter_type”, p2 = sample, value = f_type })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set filter type to “, f_type, ” for “, sample)
end
end
reaper.ImGui_EndCombo(ctx)
end
local param_defs = filter_params.filter_parameters[filter.filter_type] or {
{ name = “Cutoff Frequency”, range = { 20, 20000 } },
{ name = “Resonance”, range = { 0, 10 } }
}
if reaper.ImGui_BeginTable(ctx, “FilterParams_” .. unique_id, 2, reaper.ImGui_TableFlags_SizingStretchProp()) then
reaper.ImGui_TableSetupColumn(ctx, “Sliders”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.5)
reaper.ImGui_TableSetupColumn(ctx, “FilterDisplay”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.5)
reaper.ImGui_TableNextRow(ctx)
reaper.ImGui_TableSetColumnIndex(ctx, 0)
for _, param in ipairs(param_defs) do
local key = param.name:lower():gsub(” “, “_”)
local value = filter[key] or param.range[1]
reaper.ImGui_SetNextItemWidth(ctx, 150)
local changed, new_value = reaper.ImGui_SliderInt(ctx, param.name .. “##” .. unique_id .. “_” .. key, value, param.range[1], param.range[2])
if changed then
local new_filter = {
filter_type = filter.filter_type,
cutoff_frequency = key == “cutoff_frequency” and new_value or filter.cutoff_frequency,
resonance = key == “resonance” and new_value or filter.resonance
}
params.filter = new_filter
self.sample_parameters[sample].filter = new_filter
self.data_manager:set_sample_filter(sample, {
type = filter.filter_type,
parameters = { [“Cutoff Frequency”] = new_filter.cutoff_frequency, Resonance = new_filter.resonance }
})
filter_settings[sample] = new_filter
self.sysex:send_sysex({ p1 = “filter_param”, p2 = key, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set “, param.name, ” to “, new_value, ” for “, sample)
end
end
reaper.ImGui_TableSetColumnIndex(ctx, 1)
local filter_idx = 0
for i, f_type in ipairs(self.filter_options) do
if f_type == filter.filter_type then
filter_idx = i
break
end
end
local filter_values = {
[“Cutoff Frequency”] = filter.cutoff_frequency or 500,
Resonance = filter.resonance or 0
}
local x, y = reaper.ImGui_GetCursorScreenPos(ctx)
self.filter_display:draw(ctx, filter_idx, x, y, 300, 100, filter_values)
reaper.ImGui_EndTable(ctx)
end
reaper.ImGui_EndTabItem(ctx)
end

— Envelope Tab
if reaper.ImGui_BeginTabItem(ctx, “Envelope”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local envelope = params.envelope or {
Amplitude = { Attack = 0, Decay = 50, Sustain = 70, Release = 20, Depth = 0 },
Filter = { Attack = 0, Decay = 50, Sustain = 70, Release = 20, Depth = 0 },
Pitch = { Attack = 0, Decay = 50, Sustain = 70, Release = 20, Depth = 0 }
}
if reaper.ImGui_BeginTabBar(ctx, “EnvelopeTabs_” .. unique_id) then
for _, env_type in ipairs({ “Amplitude”, “Filter”, “Pitch” }) do
if reaper.ImGui_BeginTabItem(ctx, env_type) then
print(“KeyAssign:draw_parameters: Rendering “, env_type, ” envelope for “, sample)
if reaper.ImGui_BeginTable(ctx, env_type .. “_Table_” .. unique_id, 2, reaper.ImGui_TableFlags_SizingStretchProp()) then
reaper.ImGui_TableSetupColumn(ctx, “Sliders”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.5)
reaper.ImGui_TableSetupColumn(ctx, “EnvelopeDisplay”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.5)
reaper.ImGui_TableNextRow(ctx)
reaper.ImGui_TableSetColumnIndex(ctx, 0)
for _, param in ipairs({ “Attack”, “Decay”, “Sustain”, “Release”, “Depth” }) do
if envelope[env_type][param] then
reaper.ImGui_SetNextItemWidth(ctx, 150)
local changed, new_value = reaper.ImGui_SliderInt(ctx, param .. “##” .. unique_id .. “_” .. env_type .. “_” .. param, envelope[env_type][param], 0, 127)
if changed then
envelope[env_type][param] = new_value
params.envelope = envelope
self.sample_parameters[sample].envelope = envelope
self.data_manager:set_sample_envelopes(sample, envelope)
self.sysex:send_sysex({ p1 = “envelope”, p2 = env_type .. “_” .. param, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
if self.envelope_display.reset then
self.envelope_display:reset()
end
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Updated “, env_type, ” “, param, “=”, new_value, ” for “, sample)
end
end
end
reaper.ImGui_TableSetColumnIndex(ctx, 1)
local x, y = reaper.ImGui_GetCursorScreenPos(ctx)
local envelope_data = { [env_type] = envelope[env_type] }
print(“KeyAssign:draw_parameters: Drawing envelope for “, env_type, “: Attack=”, tostring(envelope_data[env_type].Attack or “nil”), “, Decay=”, tostring(envelope_data[env_type].Decay or “nil”))
self.envelope_display:draw(ctx, envelope_data, x, y, 300, 150)
reaper.ImGui_EndTable(ctx)
end
reaper.ImGui_EndTabItem(ctx)
end
end
reaper.ImGui_EndTabBar(ctx)
end
reaper.ImGui_EndTabItem(ctx)
end

— Loop Tab
if reaper.ImGui_BeginTabItem(ctx, “Loop”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local loop_params = params.loop or {
loop_mode = 0,
wave_start_address = { L = 0, R = 0 },
wave_length = { L = 0, R = 0 },
wave_end_address = 0,
loop_start_address = { L = 0, R = 0 },
loop_length = { L = 0, R = 0 },
loop_end_address = 0,
start_address_velocity_sensitivity = 0,
loop_tempo = 8000
}
local changed, new_value = reaper.ImGui_SliderInt(ctx, “Loop Mode##” .. unique_id, loop_params.loop_mode, 0, 5)
if changed then
loop_params.loop_mode = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_mode”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
for _, channel in ipairs({ “L”, “R” }) do
changed, new_value = reaper.ImGui_SliderInt(ctx, “Wave Start Address ” .. channel .. “##” .. unique_id .. “_” .. channel, loop_params.wave_start_address[channel], 0, 16777215)
if changed then
loop_params.wave_start_address[channel] = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “wave_start_address_” .. channel, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Wave Length ” .. channel .. “##” .. unique_id .. “_” .. channel, loop_params.wave_length[channel], 0, 16777215)
if changed then
loop_params.wave_length[channel] = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “wave_length_” .. channel, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Loop Start Address ” .. channel .. “##” .. unique_id .. “_” .. channel, loop_params.loop_start_address[channel], 0, 16777215)
if changed then
loop_params.loop_start_address[channel] = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_start_address_” .. channel, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Loop Length ” .. channel .. “##” .. unique_id .. “_” .. channel, loop_params.loop_length[channel], 0, 16777215)
if changed then
loop_params.loop_length[channel] = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_length_” .. channel, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Wave End Address##” .. unique_id, loop_params.wave_end_address, 0, 16777215)
if changed then
loop_params.wave_end_address = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “wave_end_address”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Loop End Address##” .. unique_id, loop_params.loop_end_address, 0, 16777215)
if changed then
loop_params.loop_end_address = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_end_address”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Start Address Velocity Sensitivity##” .. unique_id, loop_params.start_address_velocity_sensitivity, -63, 63)
if changed then
loop_params.start_address_velocity_sensitivity = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “start_address_velocity_sensitivity”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Loop Tempo##” .. unique_id, loop_params.loop_tempo, 8000, 15999)
if changed then
loop_params.loop_tempo = new_value
params.loop = loop_params
self.sample_parameters[sample].loop = loop_params
self.sysex:send_sysex({ p1 = “loop”, p2 = “loop_tempo”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
reaper.ImGui_EndTabItem(ctx)
end

— Playback Tab
if reaper.ImGui_BeginTabItem(ctx, “Playback”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local playback_params = params.playback or {
sample_level = 0,
pan = 0,
velocity_low_limit = 0,
velocity_offset = 0,
velocity_range_high = 127,
velocity_range_low = 0,
level_key_scaling_break_point_1 = 0,
level_key_scaling_break_point_2 = 127,
level_key_scaling_level_1 = 0,
level_key_scaling_level_2 = 0,
velocity_sensitivity = 0,
sample_portamento_type = 0,
sample_portamento_rate = 1,
sample_portamento_time = 1
}
local changed, new_value = reaper.ImGui_SliderInt(ctx, “Sample Level##” .. unique_id, playback_params.sample_level, 0, 127)
if changed then
playback_params.sample_level = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “sample_level”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Pan##” .. unique_id, playback_params.pan, -64, 63)
if changed then
playback_params.pan = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “pan”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Velocity Low Limit##” .. unique_id, playback_params.velocity_low_limit, 0, 127)
if changed then
playback_params.velocity_low_limit = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “velocity_low_limit”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Velocity Offset##” .. unique_id, playback_params.velocity_offset, -127, 127)
if changed then
playback_params.velocity_offset = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “velocity_offset”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Velocity Range High##” .. unique_id, playback_params.velocity_range_high, playback_params.velocity_range_low, 127)
if changed then
playback_params.velocity_range_high = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “velocity_range_high”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Velocity Range Low##” .. unique_id, playback_params.velocity_range_low, 0, playback_params.velocity_range_high)
if changed then
playback_params.velocity_range_low = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “velocity_range_low”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
for i = 1, 2 do
changed, new_value = reaper.ImGui_SliderInt(ctx, “Level Key Scaling Break Point ” .. i .. “##” .. unique_id .. “_” .. i, playback_params[“level_key_scaling_break_point_” .. i], i == 1 and 0 or playback_params.level_key_scaling_break_point_1, i == 1 and playback_params.level_key_scaling_break_point_2 or 127)
if changed then
playback_params[“level_key_scaling_break_point_” .. i] = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “level_key_scaling_break_point_” .. i, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Level Key Scaling Level ” .. i .. “##” .. unique_id .. “_” .. i, playback_params[“level_key_scaling_level_” .. i], 0, 127)
if changed then
playback_params[“level_key_scaling_level_” .. i] = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “level_key_scaling_level_” .. i, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Velocity Sensitivity##” .. unique_id, playback_params.velocity_sensitivity, -127, 127)
if changed then
playback_params.velocity_sensitivity = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “velocity_sensitivity”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Sample Portamento Type##” .. unique_id, playback_params.sample_portamento_type, 0, 5)
if changed then
playback_params.sample_portamento_type = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “sample_portamento_type”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Sample Portamento Rate##” .. unique_id, playback_params.sample_portamento_rate, 1, 127)
if changed then
playback_params.sample_portamento_rate = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “sample_portamento_rate”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Sample Portamento Time##” .. unique_id, playback_params.sample_portamento_time, 1, 127)
if changed then
playback_params.sample_portamento_time = new_value
params.playback = playback_params
self.sample_parameters[sample].playback = playback_params
self.sysex:send_sysex({ p1 = “playback”, p2 = “sample_portamento_time”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
reaper.ImGui_EndTabItem(ctx)
end

— Tuning Tab
if reaper.ImGui_BeginTabItem(ctx, “Tuning”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local tuning_params = params.tuning or {
pitch_bend_type = 0,
pitch_bend_range = 0,
original_key = { L = 60, R = 60 },
fine_tune = { L = 0, R = 0 },
coarse_tune = 0,
detune = 0,
dephase = 0,
expand_width = 0,
random_pitch = 0,
fixed_pitch_on = 0
}
local changed, new_value = reaper.ImGui_SliderInt(ctx, “Pitch Bend Type##” .. unique_id, tuning_params.pitch_bend_type, 0, 13)
if changed then
tuning_params.pitch_bend_type = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “pitch_bend_type”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Pitch Bend Range##” .. unique_id, tuning_params.pitch_bend_range, 0, 24)
if changed then
tuning_params.pitch_bend_range = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “pitch_bend_range”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
for _, channel in ipairs({ “L”, “R” }) do
changed, new_value = reaper.ImGui_SliderInt(ctx, “Original Key ” .. channel .. “##” .. unique_id .. “_” .. channel, tuning_params.original_key[channel], 0, 127)
if changed then
tuning_params.original_key[channel] = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “original_key_” .. channel, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Fine Tune ” .. channel .. “##” .. unique_id .. “_” .. channel, tuning_params.fine_tune[channel], -63, 63)
if changed then
tuning_params.fine_tune[channel] = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “fine_tune_” .. channel, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Coarse Tune##” .. unique_id, tuning_params.coarse_tune, -127, 127)
if changed then
tuning_params.coarse_tune = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “coarse_tune”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Detune##” .. unique_id, tuning_params.detune, -7, 7)
if changed then
tuning_params.detune = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “detune”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Dephase##” .. unique_id, tuning_params.dephase, -63, 63)
if changed then
tuning_params.dephase = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “dephase”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Expand Width##” .. unique_id, tuning_params.expand_width, -63, 63)
if changed then
tuning_params.expand_width = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “expand_width”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “Random Pitch##” .. unique_id, tuning_params.random_pitch, 0, 63)
if changed then
tuning_params.random_pitch = new_value
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “random_pitch”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_Checkbox(ctx, “Fixed Pitch On##” .. unique_id, tuning_params.fixed_pitch_on == 1)
if changed then
tuning_params.fixed_pitch_on = new_value and 1 or 0
params.tuning = tuning_params
self.sample_parameters[sample].tuning = tuning_params
self.sysex:send_sysex({ p1 = “tuning”, p2 = “fixed_pitch_on”, sample = sample, value = new_value and 1 or 0 })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
reaper.ImGui_EndTabItem(ctx)
end

— LFO Tab
if reaper.ImGui_BeginTabItem(ctx, “LFO”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local lfo_params = params.lfo or {
lfo_wave = 0,
lfo_speed = 0,
lfo_delay_time = 0,
lfo_sync_on = 0,
lfo_pitch_mod_phase_invert_on = 0,
lfo_cutoff_mod_phase_invert_on = 0
}
local changed, new_value = reaper.ImGui_SliderInt(ctx, “LFO Wave##” .. unique_id, lfo_params.lfo_wave, 0, 3)
if changed then
lfo_params.lfo_wave = new_value
params.lfo = lfo_params
self.sample_parameters[sample].lfo = lfo_params
self.sysex:send_sysex({ p1 = “lfo”, p2 = “lfo_wave”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “LFO Speed##” .. unique_id, lfo_params.lfo_speed, 0, 127)
if changed then
lfo_params.lfo_speed = new_value
params.lfo = lfo_params
self.sample_parameters[sample].lfo = lfo_params
self.sysex:send_sysex({ p1 = “lfo”, p2 = “lfo_speed”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “LFO Delay Time##” .. unique_id, lfo_params.lfo_delay_time, 0, 127)
if changed then
lfo_params.lfo_delay_time = new_value
params.lfo = lfo_params
self.sample_parameters[sample].lfo = lfo_params
self.sysex:send_sysex({ p1 = “lfo”, p2 = “lfo_delay_time”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_Checkbox(ctx, “LFO Sync On##” .. unique_id, lfo_params.lfo_sync_on == 1)
if changed then
lfo_params.lfo_sync_on = new_value and 1 or 0
params.lfo = lfo_params
self.sample_parameters[sample].lfo = lfo_params
self.sysex:send_sysex({ p1 = “lfo”, p2 = “lfo_sync_on”, sample = sample, value = new_value and 1 or 0 })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_Checkbox(ctx, “LFO Pitch Mod Phase Invert On##” .. unique_id, lfo_params.lfo_pitch_mod_phase_invert_on == 1)
if changed then
lfo_params.lfo_pitch_mod_phase_invert_on = new_value and 1 or 0
params.lfo = lfo_params
self.sample_parameters[sample].lfo = lfo_params
self.sysex:send_sysex({ p1 = “lfo”, p2 = “lfo_pitch_mod_phase_invert_on”, sample = sample, value = new_value and 1 or 0 })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
changed, new_value = reaper.ImGui_Checkbox(ctx, “LFO Cutoff Mod Phase Invert On##” .. unique_id, lfo_params.lfo_cutoff_mod_phase_invert_on == 1)
if changed then
lfo_params.lfo_cutoff_mod_phase_invert_on = new_value and 1 or 0
params.lfo = lfo_params
self.sample_parameters[sample].lfo = lfo_params
self.sysex:send_sysex({ p1 = “lfo”, p2 = “lfo_cutoff_mod_phase_invert_on”, sample = sample, value = new_value and 1 or 0 })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
end
reaper.ImGui_EndTabItem(ctx)
end

— EQ Tab
if reaper.ImGui_BeginTabItem(ctx, “EQ”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local eq_params = params.eq or {
eq_frequency = 4,
eq_gain = 52,
eq_width = 10,
eq_type = 0
}
local changed, new_value = reaper.ImGui_SliderInt(ctx, “EQ Frequency##” .. unique_id, eq_params.eq_frequency, 4, 58)
if changed then
eq_params.eq_frequency = new_value
params.eq = eq_params
self.sample_parameters[sample].eq = eq_params
self.sysex:send_sysex({ p1 = “eq”, p2 = “eq_frequency”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set EQ frequency to “, new_value, ” for “, sample)
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “EQ Gain##” .. unique_id, eq_params.eq_gain, 52, 76)
if changed then
eq_params.eq_gain = new_value
params.eq = eq_params
self.sample_parameters[sample].eq = eq_params
self.sysex:send_sysex({ p1 = “eq”, p2 = “eq_gain”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set EQ gain to “, new_value, ” for “, sample)
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “EQ Width##” .. unique_id, eq_params.eq_width, 0, 127)
if changed then
eq_params.eq_width = new_value
params.eq = eq_params
self.sample_parameters[sample].eq = eq_params
self.sysex:send_sysex({ p1 = “eq”, p2 = “eq_width”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set EQ width to “, new_value, ” for “, sample)
end
changed, new_value = reaper.ImGui_SliderInt(ctx, “EQ Type##” .. unique_id, eq_params.eq_type, 0, 3)
if changed then
eq_params.eq_type = new_value
params.eq = eq_params
self.sample_parameters[sample].eq = eq_params
self.sysex:send_sysex({ p1 = “eq”, p2 = “eq_type”, sample = sample, value = new_value })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set EQ type to “, new_value, ” for “, sample)
end
reaper.ImGui_EndTabItem(ctx)
end

— Modulation Matrix Tab
if reaper.ImGui_BeginTabItem(ctx, “Mod Matrix”) then
local unique_id = self.selected_note .. “_” .. self.selected_layer .. “_” .. sample
local mod_matrix = params.mod_matrix or {
sources = { “LFO”, “Velocity”, “Mod Wheel”, “Aftertouch” },
destinations = { “Pitch”, “Filter Cutoff”, “Amplitude”, “Pan” },
assignments = {}
}
reaper.ImGui_Text(ctx, “Modulation Matrix”)
if reaper.ImGui_BeginTable(ctx, “ModMatrix_” .. unique_id, 3, reaper.ImGui_TableFlags_Borders() | reaper.ImGui_TableFlags_SizingStretchProp()) then
reaper.ImGui_TableSetupColumn(ctx, “Source”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.3)
reaper.ImGui_TableSetupColumn(ctx, “Destination”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.3)
reaper.ImGui_TableSetupColumn(ctx, “Amount”, reaper.ImGui_TableColumnFlags_WidthStretch(), 0.4)
reaper.ImGui_TableHeadersRow(ctx)
for i, assignment in ipairs(mod_matrix.assignments) do
reaper.ImGui_TableNextRow(ctx)
reaper.ImGui_TableSetColumnIndex(ctx, 0)
reaper.ImGui_SetNextItemWidth(ctx, -1)
if reaper.ImGui_BeginCombo(ctx, “##Source_” .. i .. “_” .. unique_id, assignment.source or mod_matrix.sources[1]) then
for _, source in ipairs(mod_matrix.sources) do
if reaper.ImGui_Selectable(ctx, source, assignment.source == source) then
assignment.source = source
params.mod_matrix = mod_matrix
self.sample_parameters[sample].mod_matrix = mod_matrix
self.sysex:send_sysex({ p1 = “mod_matrix”, p2 = “source”, sample = sample, index = i, value = source })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set mod matrix source to “, source, ” for “, sample)
end
end
reaper.ImGui_EndCombo(ctx)
end
reaper.ImGui_TableSetColumnIndex(ctx, 1)
reaper.ImGui_SetNextItemWidth(ctx, -1)
if reaper.ImGui_BeginCombo(ctx, “##Dest_” .. i .. “_” .. unique_id, assignment.destination or mod_matrix.destinations[1]) then
for _, dest in ipairs(mod_matrix.destinations) do
if reaper.ImGui_Selectable(ctx, dest, assignment.destination == dest) then
assignment.destination = dest
params.mod_matrix = mod_matrix
self.sample_parameters[sample].mod_matrix = mod_matrix
self.sysex:send_sysex({ p1 = “mod_matrix”, p2 = “destination”, sample = sample, index = i, value = dest })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set mod matrix destination to “, dest, ” for “, sample)
end
end
reaper.ImGui_EndCombo(ctx)
end
reaper.ImGui_TableSetColumnIndex(ctx, 2)
reaper.ImGui_SetNextItemWidth(ctx, -1)
local changed, new_amount = reaper.ImGui_SliderInt(ctx, “##Amount_” .. i .. “_” .. unique_id, assignment.amount or 0, -100, 100)
if changed then
assignment.amount = new_amount
params.mod_matrix = mod_matrix
self.sample_parameters[sample].mod_matrix = mod_matrix
self.sysex:send_sysex({ p1 = “mod_matrix”, p2 = “amount”, sample = sample, index = i, value = new_amount })
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Set mod matrix amount to “, new_amount, ” for “, sample)
end
end
reaper.ImGui_TableNextRow(ctx)
reaper.ImGui_TableSetColumnIndex(ctx, 0)
if reaper.ImGui_Button(ctx, “Add Assignment##” .. unique_id) then
table.insert(mod_matrix.assignments, { source = mod_matrix.sources[1], destination = mod_matrix.destinations[1], amount = 0 })
params.mod_matrix = mod_matrix
self.sample_parameters[sample].mod_matrix = mod_matrix
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Added new mod matrix assignment for “, sample)
end
reaper.ImGui_TableSetColumnIndex(ctx, 1)
if reaper.ImGui_Button(ctx, “Remove Last##” .. unique_id) and #mod_matrix.assignments > 0 then
table.remove(mod_matrix.assignments)
params.mod_matrix = mod_matrix
self.sample_parameters[sample].mod_matrix = mod_matrix
self:save_sample_parameters(sample)
self:save_program_state()
self.pending_refresh = true
print(“KeyAssign:draw_parameters: Removed last mod matrix assignment for “, sample)
end
reaper.ImGui_EndTable(ctx)
end
reaper.ImGui_EndTabItem(ctx)
end
reaper.ImGui_EndTabBar(ctx)
end
end)
if not success then
print(“KeyAssign:draw_parameters: Error: “, err)
reaper.ImGui_Text(ctx, “Error: Failed to render parameters (” .. tostring(err) .. “)”)
end
end

function KeyAssign:auto_assign_samples(samples)
self:push_undo_state(“Auto-assign samples”)
local normalized_samples, display_names = self:normalize_sample_names(samples)
self.sample_display_names = display_names
self.sample_list = normalized_samples
local start_note = self.auto_assign_start_note
local range_low = self.auto_assign_range_low
local range_high = self.auto_assign_range_high
local mapping = self.mapping_options[self.auto_assign_mapping + 1]
local num_samples = #normalized_samples

— Clear existing assignments
for note = 0, 127 do
self.key_assignments[note] = { samples = {}, velocity_ranges = {}, note_range = {}, crossfades = { key = 0, velocity = 0 }, direction = “Forward”, round_robin = false }
end

if num_samples == 0 then
self.data_manager.status_message = “No samples to assign”
self:save_program_state()
self.pending_refresh = true
return
end

if mapping == “Chromatic” then
local note = start_note
for i, sample in ipairs(normalized_samples) do
if note <= range_high then self.key_assignments[note].samples = { sample } self.key_assignments[note].velocity_ranges = { { min = 0, max = 127 } } self.key_assignments[note].note_range = { { low = note, high = note } } self.sysex:send_sysex({ p1 = "note", p2 = "sample", note = note, layer = 1, value = sample }) self.sysex:send_sysex({ p1 = "note", p2 = "velocity_min", note = note, layer = 1, value = 0 }) self.sysex:send_sysex({ p1 = "note", p2 = "velocity_max", note = note, layer = 1, value = 127 }) self.sysex:send_sysex({ p1 = "note", p2 = "note_range_low", note = note, layer = 1, value = note }) self.sysex:send_sysex({ p1 = "note", p2 = "note_range_high", note = note, layer = 1, value = note }) note = note + 1 end end elseif mapping == "Velocity" then local velocity_step = 127 / num_samples for note = range_low, range_high do for i, sample in ipairs(normalized_samples) do table.insert(self.key_assignments[note].samples, sample) table.insert(self.key_assignments[note].velocity_ranges, { min = math.floor((i-1) * velocity_step), max = math.floor(i * velocity_step) }) table.insert(self.key_assignments[note].note_range, { low = note, high = note }) self.sysex:send_sysex({ p1 = "note", p2 = "sample", note = note, layer = i, value = sample }) self.sysex:send_sysex({ p1 = "note", p2 = "velocity_min", note = note, layer = i, value = math.floor((i-1) * velocity_step) }) self.sysex:send_sysex({ p1 = "note", p2 = "velocity_max", note = note, layer = i, value = math.floor(i * velocity_step) }) self.sysex:send_sysex({ p1 = "note", p2 = "note_range_low", note = note, layer = i, value = note }) self.sysex:send_sysex({ p1 = "note", p2 = "note_range_high", note = note, layer = i, value = note }) end end elseif mapping == "Random" then for note = range_low, range_high do local sample = normalized_samples[math.random(1, num_samples)] self.key_assignments[note].samples = { sample } self.key_assignments[note].velocity_ranges = { { min = 0, max = 127 } } self.key_assignments[note].note_range = { { low = note, high = note } } self.sysex:send_sysex({ p1 = "note", p2 = "sample", note = note, layer = 1, value = sample }) self.sysex:send_sysex({ p1 = "note", p2 = "velocity_min", note = note, layer = 1, value = 0 }) self.sysex:send_sysex({ p1 = "note", p2 = "velocity_max", note = note, layer = 1, value = 127 }) self.sysex:send_sysex({ p1 = "note", p2 = "note_range_low", note = note, layer = 1, value = note }) self.sysex:send_sysex({ p1 = "note", p2 = "note_range_high", note = note, layer = 1, value = note }) end end-- Apply additional auto-assign options for note = range_low, range_high do local data = self.key_assignments[note] if #data.samples > 0 then
if self.auto_assign_crossfade then
data.crossfades = { key = 10, velocity = 10 }
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_key”, note = note, value = 10 })
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_velocity”, note = note, value = 10 })
end
if self.auto_assign_random_direction then
data.direction = self.direction_options[math.random(1, #self.direction_options)]
self.sysex:send_sysex({ p1 = “note”, p2 = “direction”, note = note, value = data.direction })
end
if self.auto_assign_round_robin then
data.round_robin = true
self.sysex:send_sysex({ p1 = “note”, p2 = “round_robin”, note = note, value = 1 })
end
if self.auto_assign_layers then
for i = 1, math.random(1, 3) do
local extra_sample = normalized_samples[math.random(1, num_samples)]
table.insert(data.samples, extra_sample)
table.insert(data.velocity_ranges, { min = math.random(0, 63), max = math.random(64, 127) })
table.insert(data.note_range, { low = note, high = note })
self.sysex:send_sysex({ p1 = “note”, p2 = “sample”, note = note, layer = #data.samples, value = extra_sample })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_min”, note = note, layer = #data.samples, value = data.velocity_ranges[#data.samples].min })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_max”, note = note, layer = #data.samples, value = data.velocity_ranges[#data.samples].max })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_low”, note = note, layer = #data.samples, value = note })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_high”, note = note, layer = #data.samples, value = note })
end
end
if self.auto_assign_random_pitch then
for _, s in ipairs(data.samples) do
self.sample_parameters[s].tuning = self.sample_parameters[s].tuning or {}
self.sample_parameters[s].tuning.random_pitch = math.random(0, 63)
self.sysex:send_sysex({ p1 = “tuning”, p2 = “random_pitch”, sample = s, value = self.sample_parameters[s].tuning.random_pitch })
end
end
end
end

— Initialize parameters for assigned samples
for _, sample in ipairs(normalized_samples) do
self:initialize_sample_parameters(self.current_program, sample)
self:save_sample_parameters(sample)
end

self:save_program_state()
self.pending_refresh = true
self.data_manager.status_message = “Auto-assigned ” .. num_samples .. ” samples”
print(“KeyAssign:auto_assign_samples: Assigned “, num_samples, ” samples with “, mapping, ” mapping”)
end

function KeyAssign:table_contains(tbl, element)
for _, value in ipairs(tbl) do
if value == element then
return true
end
end
return false
end

function KeyAssign:normalize_sample_names(samples)
local normalized_samples = {}
local display_names = {}
for _, sample in ipairs(samples) do
local norm = self:normalize_sample_name(sample)
if not self:table_contains(normalized_samples, norm) then
table.insert(normalized_samples, norm)
display_names[norm] = sample
end
end
return normalized_samples, display_names
end

function KeyAssign:normalize_sample_name(sample)
return tostring(sample):gsub(“[^%w]”, “_”):lower()
end

function KeyAssign:copy_note(note)
if not self.key_assignments[note] then
self.data_manager.status_message = “No data to copy for note ” .. note
print(“KeyAssign:copy_note: No data for note “, note)
return
end
self:push_undo_state(“Copy note ” .. note)
self.copied_note = self:deep_copy(self.key_assignments[note])
self.copied_pane = note
self.data_manager.status_message = “Copied note ” .. note
print(“KeyAssign:copy_note: Copied note “, note)
end

function KeyAssign:paste_note(target_note)
if not self.copied_note then
self.data_manager.status_message = “No note data to paste”
print(“KeyAssign:paste_note: No copied note data”)
return
end
self:push_undo_state(“Paste to note ” .. target_note)
self.key_assignments[target_note] = self:deep_copy(self.copied_note)
for i, sample in ipairs(self.key_assignments[target_note].samples) do
self:initialize_sample_parameters(self.current_program, sample)
self.sysex:send_sysex({ p1 = “note”, p2 = “sample”, note = target_note, layer = i, value = sample })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_min”, note = target_note, layer = i, value = self.key_assignments[target_note].velocity_ranges[i].min })
self.sysex:send_sysex({ p1 = “note”, p2 = “velocity_max”, note = target_note, layer = i, value = self.key_assignments[target_note].velocity_ranges[i].max })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_low”, note = target_note, layer = i, value = self.key_assignments[target_note].note_range[i].low })
self.sysex:send_sysex({ p1 = “note”, p2 = “note_range_high”, note = target_note, layer = i, value = self.key_assignments[target_note].note_range[i].high })
end
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_key”, note = target_note, value = self.key_assignments[target_note].crossfades.key })
self.sysex:send_sysex({ p1 = “note”, p2 = “crossfade_velocity”, note = target_note, value = self.key_assignments[target_note].crossfades.velocity })
self.sysex:send_sysex({ p1 = “note”, p2 = “direction”, note = target_note, value = self.key_assignments[target_note].direction })
self.sysex:send_sysex({ p1 = “note”, p2 = “round_robin”, note = target_note, value = self.key_assignments[target_note].round_robin and 1 or 0 })
self:save_program_state()
self.pending_refresh = true
self.data_manager.status_message = “Pasted to note ” .. target_note
print(“KeyAssign:paste_note: Pasted to note “, target_note)
end

function KeyAssign:handle_midi_note(note, velocity)
if note < 0 or note > 127 then
print(“KeyAssign:handle_midi_note: Invalid note “, note)
return
end
local data = self.key_assignments[note]
if not data or #data.samples == 0 then
print(“KeyAssign:handle_midi_note: No samples for note “, note)
return
end
for i, sample in ipairs(data.samples) do
local vel_range = data.velocity_ranges[i] or { min = 0, max = 127 }
if velocity >= vel_range.min and velocity <= vel_range.max then local direction = data.direction or "Forward" self.sysex:send_midi_note({ note = note, velocity = velocity, sample = sample, direction = direction }) print("KeyAssign:handle_midi_note: Sent MIDI note ", note, " velocity ", velocity, " sample ", sample) end end endfunction KeyAssign:refresh_ui() self.pending_refresh = true self.data_manager.refresh_ui = true print("KeyAssign:refresh_ui: Requested UI refresh") endreturn KeyAssign

0:00
0:00
0
    Your Cart
    Your cart is emptyReturn to Shop